summaryrefslogtreecommitdiff
path: root/python/ovs/db
diff options
context:
space:
mode:
authorBen Pfaff <blp@nicira.com>2010-08-25 10:26:40 -0700
committerBen Pfaff <blp@nicira.com>2010-08-25 14:55:48 -0700
commit991559357f6a03c3a5b70c053c8c2554aa8d5ee4 (patch)
tree8731002433c65ea41dbe94648c0e39737f666469 /python/ovs/db
parentd1b680c61626595b2777f4bf25997a9178acb60c (diff)
downloadopenvswitch-991559357f6a03c3a5b70c053c8c2554aa8d5ee4.tar.gz
Implement initial Python bindings for Open vSwitch database.
These initial bindings pass a few hundred of the corresponding tests for C implementations of various bits of the Open vSwitch library API. The poorest part of them is actually the Python IDL interface in ovs.db.idl, which has not received enough attention yet. It appears to work, but it doesn't yet support writes (transactions) and it is difficult to use. I hope to improve it as it becomes clear what semantics Python applications actually want from an IDL.
Diffstat (limited to 'python/ovs/db')
-rw-r--r--python/ovs/db/__init__.py1
-rw-r--r--python/ovs/db/data.py433
-rw-r--r--python/ovs/db/error.py34
-rw-r--r--python/ovs/db/idl.py305
-rw-r--r--python/ovs/db/parser.py105
-rw-r--r--python/ovs/db/schema.py159
-rw-r--r--python/ovs/db/types.py545
7 files changed, 1582 insertions, 0 deletions
diff --git a/python/ovs/db/__init__.py b/python/ovs/db/__init__.py
new file mode 100644
index 000000000..218d8921e
--- /dev/null
+++ b/python/ovs/db/__init__.py
@@ -0,0 +1 @@
+# This file intentionally left blank.
diff --git a/python/ovs/db/data.py b/python/ovs/db/data.py
new file mode 100644
index 000000000..bfdc11cbc
--- /dev/null
+++ b/python/ovs/db/data.py
@@ -0,0 +1,433 @@
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import errno
+import logging
+import os
+import re
+import select
+import sys
+import uuid
+
+import ovs.poller
+import ovs.socket_util
+import ovs.json
+import ovs.jsonrpc
+import ovs.ovsuuid
+
+import ovs.db.parser
+from ovs.db import error
+import ovs.db.types
+
+class ConstraintViolation(error.Error):
+ def __init__(self, msg, json=None):
+ error.Error.__init__(self, msg, json, tag="constraint violation")
+
+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 Atom(object):
+ def __init__(self, type, value=None):
+ self.type = type
+ if value is not None:
+ self.value = value
+ else:
+ self.value = type.default_atom()
+
+ def __cmp__(self, other):
+ if not isinstance(other, Atom) or self.type != other.type:
+ return NotImplemented
+ elif self.value < other.value:
+ return -1
+ elif self.value > other.value:
+ return 1
+ else:
+ return 0
+
+ def __hash__(self):
+ return hash(self.value)
+
+ @staticmethod
+ def default(type):
+ return Atom(type)
+
+ def is_default(self):
+ return self == default(self.type)
+
+ @staticmethod
+ def from_json(base, json, symtab=None):
+ type_ = base.type
+ json = ovs.db.parser.float_to_int(json)
+ if ((type_ == ovs.db.types.IntegerType and type(json) in [int, long])
+ or (type_ == ovs.db.types.RealType and type(json) in [int, long, float])
+ or (type_ == ovs.db.types.BooleanType and type(json) == bool)
+ or (type_ == ovs.db.types.StringType and type(json) in [str, unicode])):
+ atom = Atom(type_, json)
+ elif type_ == ovs.db.types.UuidType:
+ atom = Atom(type_, ovs.ovsuuid.UUID.from_json(json, symtab))
+ else:
+ raise error.Error("expected %s" % type_.to_string(), json)
+ atom.check_constraints(base)
+ return atom
+
+ def check_constraints(self, base):
+ """Checks whether 'atom' meets the constraints (if any) defined in
+ 'base' and raises an ovs.db.error.Error if any constraint is violated.
+
+ 'base' and 'atom' must have the same type.
+
+ Checking UUID constraints is deferred to transaction commit time, so
+ this function does nothing for UUID constraints."""
+ assert base.type == self.type
+ if base.enum is not None and self not in base.enum:
+ raise ConstraintViolation(
+ "%s is not one of the allowed values (%s)"
+ % (self.to_string(), base.enum.to_string()))
+ elif base.type in [ovs.db.types.IntegerType, ovs.db.types.RealType]:
+ if ((base.min is None or self.value >= base.min) and
+ (base.max is None or self.value <= base.max)):
+ pass
+ elif base.min is not None and base.max is not None:
+ raise ConstraintViolation(
+ "%s is not in the valid range %.15g to %.15g (inclusive)"
+ % (self.to_string(), base.min, base.max))
+ elif base.min is not None:
+ raise ConstraintViolation(
+ "%s is less than minimum allowed value %.15g"
+ % (self.to_string(), base.min))
+ else:
+ raise ConstraintViolation(
+ "%s is greater than maximum allowed value %.15g"
+ % (self.to_string(), base.max))
+ elif base.type == ovs.db.types.StringType:
+ # XXX The C version validates that the string is valid UTF-8 here.
+ # Do we need to do that in Python too?
+ s = self.value
+ length = len(s)
+ if length < base.min_length:
+ raise ConstraintViolation(
+ "\"%s\" length %d is less than minimum allowed length %d"
+ % (s, length, base.min_length))
+ elif length > base.max_length:
+ raise ConstraintViolation(
+ "\"%s\" length %d is greater than maximum allowed "
+ "length %d" % (s, length, base.max_length))
+
+ def to_json(self):
+ if self.type == ovs.db.types.UuidType:
+ return self.value.to_json()
+ else:
+ return self.value
+
+ def cInitAtom(self, var):
+ if self.type == ovs.db.types.IntegerType:
+ return ['%s.integer = %d;' % (var, self.value)]
+ elif self.type == ovs.db.types.RealType:
+ return ['%s.real = %.15g;' % (var, self.value)]
+ elif self.type == ovs.db.types.BooleanType:
+ if self.value:
+ return ['%s.boolean = true;']
+ else:
+ return ['%s.boolean = false;']
+ elif self.type == ovs.db.types.StringType:
+ return ['%s.string = xstrdup("%s");'
+ % (var, escapeCString(self.value))]
+ elif self.type == ovs.db.types.UuidType:
+ return self.value.cInitUUID(var)
+
+ def toEnglish(self, escapeLiteral=returnUnchanged):
+ if self.type == ovs.db.types.IntegerType:
+ return '%d' % self.value
+ elif self.type == ovs.db.types.RealType:
+ return '%.15g' % self.value
+ elif self.type == ovs.db.types.BooleanType:
+ if self.value:
+ return 'true'
+ else:
+ return 'false'
+ elif self.type == ovs.db.types.StringType:
+ return escapeLiteral(self.value)
+ elif self.type == ovs.db.types.UuidType:
+ return self.value.value
+
+ __need_quotes_re = re.compile("$|true|false|[^_a-zA-Z]|.*[^-._a-zA-Z]")
+ @staticmethod
+ def __string_needs_quotes(s):
+ return Atom.__need_quotes_re.match(s)
+
+ def to_string(self):
+ if self.type == ovs.db.types.IntegerType:
+ return '%d' % self.value
+ elif self.type == ovs.db.types.RealType:
+ return '%.15g' % self.value
+ elif self.type == ovs.db.types.BooleanType:
+ if self.value:
+ return 'true'
+ else:
+ return 'false'
+ elif self.type == ovs.db.types.StringType:
+ if Atom.__string_needs_quotes(self.value):
+ return ovs.json.to_string(self.value)
+ else:
+ return self.value
+ elif self.type == ovs.db.types.UuidType:
+ return str(self.value)
+
+ @staticmethod
+ def new(x):
+ if type(x) in [int, long]:
+ t = ovs.db.types.IntegerType
+ elif type(x) == float:
+ t = ovs.db.types.RealType
+ elif x in [False, True]:
+ t = ovs.db.types.RealType
+ elif type(x) in [str, unicode]:
+ t = ovs.db.types.StringType
+ elif isinstance(x, uuid):
+ t = ovs.db.types.UuidType
+ else:
+ raise TypeError
+ return Atom(t, x)
+
+class Datum(object):
+ def __init__(self, type, values={}):
+ self.type = type
+ self.values = values
+
+ def __cmp__(self, other):
+ if not isinstance(other, Datum):
+ return NotImplemented
+ elif self.values < other.values:
+ return -1
+ elif self.values > other.values:
+ return 1
+ else:
+ return 0
+
+ __hash__ = None
+
+ def __contains__(self, item):
+ return item in self.values
+
+ def clone(self):
+ return Datum(self.type, dict(self.values))
+
+ @staticmethod
+ def default(type):
+ if type.n_min == 0:
+ values = {}
+ elif type.is_map():
+ values = {type.key.default(): type.value.default()}
+ else:
+ values = {type.key.default(): None}
+ return Datum(type, values)
+
+ @staticmethod
+ def is_default(self):
+ return self == default(self.type)
+
+ def check_constraints(self):
+ """Checks that each of the atoms in 'datum' conforms to the constraints
+ specified by its 'type' and raises an ovs.db.error.Error.
+
+ This function is not commonly useful because the most ordinary way to
+ obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
+ which check constraints themselves."""
+ for keyAtom, valueAtom in self.values:
+ keyAtom.check_constraints()
+ if valueAtom is not None:
+ valueAtom.check_constraints()
+
+ @staticmethod
+ def from_json(type_, json, symtab=None):
+ """Parses 'json' as a datum of the type described by 'type'. If
+ successful, returns a new datum. On failure, raises an
+ ovs.db.error.Error.
+
+ Violations of constraints expressed by 'type' are treated as errors.
+
+ If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
+ Refer to ovsdb/SPECS for information about this, and for the syntax
+ that this function accepts."""
+ is_map = type_.is_map()
+ if (is_map or
+ (type(json) == list and len(json) > 0 and json[0] == "set")):
+ if is_map:
+ class_ = "map"
+ else:
+ class_ = "set"
+
+ inner = ovs.db.parser.unwrap_json(json, class_, list)
+ n = len(inner)
+ if n < type_.n_min or n > type_.n_max:
+ raise error.Error("%s must have %d to %d members but %d are "
+ "present" % (class_, type_.n_min,
+ type_.n_max, n),
+ json)
+
+ values = {}
+ for element in inner:
+ if is_map:
+ key, value = ovs.db.parser.parse_json_pair(element)
+ keyAtom = Atom.from_json(type_.key, key, symtab)
+ valueAtom = Atom.from_json(type_.value, value, symtab)
+ else:
+ keyAtom = Atom.from_json(type_.key, element, symtab)
+ valueAtom = None
+
+ if keyAtom in values:
+ if is_map:
+ raise error.Error("map contains duplicate key")
+ else:
+ raise error.Error("set contains duplicate")
+
+ values[keyAtom] = valueAtom
+
+ return Datum(type_, values)
+ else:
+ keyAtom = Atom.from_json(type_.key, json, symtab)
+ return Datum(type_, {keyAtom: None})
+
+ def to_json(self):
+ if len(self.values) == 1 and not self.type.is_map():
+ key = self.values.keys()[0]
+ return key.to_json()
+ elif not self.type.is_map():
+ return ["set", [k.to_json() for k in sorted(self.values.keys())]]
+ else:
+ return ["map", [[k.to_json(), v.to_json()]
+ for k, v in sorted(self.values.items())]]
+
+ def to_string(self):
+ if self.type.n_max > 1 or len(self.values) == 0:
+ if self.type.is_map():
+ s = "{"
+ else:
+ s = "["
+ else:
+ s = ""
+
+ i = 0
+ for key in sorted(self.values):
+ if i > 0:
+ s += ", "
+ i += 1
+
+ if self.type.is_map():
+ s += "%s=%s" % (key.to_string(), self.values[key].to_string())
+ else:
+ s += key.to_string()
+
+ if self.type.n_max > 1 or len(self.values) == 0:
+ if self.type.is_map():
+ s += "}"
+ else:
+ s += "]"
+ return s
+
+ def as_list(self):
+ if self.type.is_map():
+ return [[k.value, v.value] for k, v in self.values.iteritems()]
+ else:
+ return [k.value for k in self.values.iterkeys()]
+
+ def as_scalar(self):
+ if len(self.values) == 1:
+ if self.type.is_map():
+ k, v = self.values.iteritems()[0]
+ return [k.value, v.value]
+ else:
+ return self.values.keys()[0].value
+ else:
+ return None
+
+ def __getitem__(self, key):
+ if not isinstance(key, Atom):
+ key = Atom.new(key)
+ if not self.type.is_map():
+ raise IndexError
+ elif key not in self.values:
+ raise KeyError
+ else:
+ return self.values[key].value
+
+ def get(self, key, default=None):
+ if not isinstance(key, Atom):
+ key = Atom.new(key)
+ if key in self.values:
+ return self.values[key].value
+ else:
+ return default
+
+ def __str__(self):
+ return self.to_string()
+
+ def conforms_to_type(self):
+ n = len(self.values)
+ return n >= self.type.n_min and n <= self.type.n_max
+
+ 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)]
+
+ i = 0
+ for key, value in sorted(self.values.items()):
+ s += key.cInitAtom("%s->keys[%d]" % (var, i))
+ i += 1
+
+ if self.type.value:
+ s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
+ % (var, len(self.values), var)]
+ i = 0
+ for key, value in sorted(self.values.items()):
+ s += value.cInitAtom("%s->values[%d]" % (var, i))
+ i += 1
+ else:
+ s += ["%s->values = NULL;" % var]
+
+ if len(self.values) > 1:
+ s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
+ % (var, self.type.key.type.to_string().upper())]
+
+ return s
diff --git a/python/ovs/db/error.py b/python/ovs/db/error.py
new file mode 100644
index 000000000..084db6e2e
--- /dev/null
+++ b/python/ovs/db/error.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import ovs.json
+
+class Error(Exception):
+ def __init__(self, msg, json=None, tag=None):
+ Exception.__init__(self)
+ self.msg = msg
+ self.json = json
+ if tag is None:
+ if json is None:
+ self.tag = "ovsdb error"
+ else:
+ self.tag = "syntax error"
+ else:
+ self.tag = tag
+
+ def __str__(self):
+ syntax = ""
+ if self.json is not None:
+ syntax = "syntax \"%s\": " % ovs.json.to_string(self.json)
+ return "%s%s: %s" % (syntax, self.tag, self.msg)
diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
new file mode 100644
index 000000000..5260d983f
--- /dev/null
+++ b/python/ovs/db/idl.py
@@ -0,0 +1,305 @@
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+import ovs.jsonrpc
+import ovs.db.schema
+from ovs.db import error
+import ovs.ovsuuid
+
+class Idl:
+ """Open vSwitch Database Interface Definition Language (OVSDB IDL).
+
+ The OVSDB IDL maintains an in-memory replica of a database. It issues RPC
+ requests to an OVSDB database server and parses the responses, converting
+ raw JSON into data structures that are easier for clients to digest.
+
+ The IDL also assists with issuing database transactions. The client
+ creates a transaction, manipulates the IDL data structures, and commits or
+ aborts the transaction. The IDL then composes and issues the necessary
+ JSON-RPC requests and reports to the client whether the transaction
+ completed successfully.
+
+ If 'schema_cb' is provided, it should be a callback function that accepts
+ an ovs.db.schema.DbSchema as its argument. It should determine whether the
+ schema is acceptable and raise an ovs.db.error.Error if it is not. It may
+ also delete any tables or columns from the schema that the client has no
+ interest in monitoring, to save time and bandwidth during monitoring. Its
+ return value is ignored."""
+
+ def __init__(self, remote, db_name, schema_cb=None):
+ """Creates and returns a connection to the database named 'db_name' on
+ 'remote', which should be in a form acceptable to
+ ovs.jsonrpc.session.open(). The connection will maintain an in-memory
+ replica of the remote database."""
+ self.remote = remote
+ self.session = ovs.jsonrpc.Session.open(remote)
+ self.db_name = db_name
+ self.last_seqno = None
+ self.schema = None
+ self.state = None
+ self.change_seqno = 0
+ self.data = {}
+ self.schema_cb = schema_cb
+
+ def close(self):
+ self.session.close()
+
+ def run(self):
+ """Processes a batch of messages from the database server. Returns
+ True if the database as seen through the IDL changed, False if it did
+ not change. The initial fetch of the entire contents of the remote
+ database is considered to be one kind of change.
+
+ This function can return occasional false positives, that is, report
+ that the database changed even though it didn't. This happens if the
+ connection to the database drops and reconnects, which causes the
+ database contents to be reloaded even if they didn't change. (It could
+ also happen if the database server sends out a "change" that reflects
+ what we already thought was in the database, but the database server is
+ not supposed to do that.)
+
+ As an alternative to checking the return value, the client may check
+ for changes in the value returned by self.get_seqno()."""
+ initial_change_seqno = self.change_seqno
+ self.session.run()
+ if self.session.is_connected():
+ seqno = self.session.get_seqno()
+ if seqno != self.last_seqno:
+ self.last_seqno = seqno
+ self.state = (self.__send_schema_request, None)
+ if self.state:
+ self.state[0]()
+ return initial_change_seqno != self.change_seqno
+
+ def wait(self, poller):
+ """Arranges for poller.block() to wake up when self.run() has something
+ to do or when activity occurs on a transaction on 'self'."""
+ self.session.wait(poller)
+ if self.state and self.state[1]:
+ self.state[1](poller)
+
+ def get_seqno(self):
+ """Returns a number that represents the IDL's state. When the IDL
+ updated (by self.run()), the return value changes."""
+ return self.change_seqno
+
+ def __send_schema_request(self):
+ msg = ovs.jsonrpc.Message.create_request("get_schema", [self.db_name])
+ self.session.send(msg)
+ self.state = (lambda: self.__recv_schema(msg.id), self.__recv_wait)
+
+ def __recv_schema(self, id):
+ msg = self.session.recv()
+ if msg and msg.type == ovs.jsonrpc.Message.T_REPLY and msg.id == id:
+ try:
+ self.schema = ovs.db.schema.DbSchema.from_json(msg.result)
+ except error.Error, e:
+ logging.error("%s: parse error in received schema: %s"
+ % (self.remote, e))
+ self.__error()
+ return
+
+ if self.schema_cb:
+ try:
+ self.schema_cb(self.schema)
+ except error.Error, e:
+ logging.error("%s: error validating schema: %s"
+ % (self.remote, e))
+ self.__error()
+ return
+
+ self.__send_monitor_request()
+ elif msg:
+ logging.error("%s: unexpected message expecting schema: %s"
+ % (self.remote, msg))
+ self.__error()
+
+ def __recv_wait(self, poller):
+ self.session.recv_wait(poller)
+
+ def __send_monitor_request(self):
+ monitor_requests = {}
+ for table in self.schema.tables.itervalues():
+ monitor_requests[table.name] = {"columns": table.columns.keys()}
+ msg = ovs.jsonrpc.Message.create_request(
+ "monitor", [self.db_name, None, monitor_requests])
+ self.session.send(msg)
+ self.state = (lambda: self.__recv_monitor_reply(msg.id),
+ self.__recv_wait)
+
+ def __recv_monitor_reply(self, id):
+ msg = self.session.recv()
+ if msg and msg.type == ovs.jsonrpc.Message.T_REPLY and msg.id == id:
+ try:
+ self.change_seqno += 1
+ self.state = (self.__recv_update, self.__recv_wait)
+ self.__clear()
+ self.__parse_update(msg.result)
+ except error.Error, e:
+ logging.error("%s: parse error in received schema: %s"
+ % (self.remote, e))
+ self.__error()
+ elif msg:
+ logging.error("%s: unexpected message expecting schema: %s"
+ % (self.remote, msg))
+ self.__error()
+
+ def __recv_update(self):
+ msg = self.session.recv()
+ if (msg and msg.type == ovs.jsonrpc.Message.T_NOTIFY and
+ type(msg.params) == list and len(msg.params) == 2 and
+ msg.params[0] is None):
+ self.__parse_update(msg.params[1])
+ elif msg:
+ logging.error("%s: unexpected message expecting update: %s"
+ % (self.remote, msg))
+ self.__error()
+
+ def __error(self):
+ self.session.force_reconnect()
+
+ def __parse_update(self, update):
+ try:
+ self.__do_parse_update(update)
+ except error.Error, e:
+ logging.error("%s: error parsing update: %s" % (self.remote, e))
+
+ def __do_parse_update(self, table_updates):
+ if type(table_updates) != dict:
+ raise error.Error("<table-updates> is not an object",
+ table_updates)
+
+ for table_name, table_update in table_updates.iteritems():
+ table = self.schema.tables.get(table_name)
+ if not table:
+ raise error.Error("<table-updates> includes unknown "
+ "table \"%s\"" % table_name)
+
+ if type(table_update) != dict:
+ raise error.Error("<table-update> for table \"%s\" is not "
+ "an object" % table_name, table_update)
+
+ for uuid_string, row_update in table_update.iteritems():
+ if not ovs.ovsuuid.UUID.is_valid_string(uuid_string):
+ raise error.Error("<table-update> for table \"%s\" "
+ "contains bad UUID \"%s\" as member "
+ "name" % (table_name, uuid_string),
+ table_update)
+ uuid = ovs.ovsuuid.UUID.from_string(uuid_string)
+
+ if type(row_update) != dict:
+ raise error.Error("<table-update> for table \"%s\" "
+ "contains <row-update> for %s that "
+ "is not an object"
+ % (table_name, uuid_string))
+
+ old = row_update.get("old", None)
+ new = row_update.get("new", None)
+
+ if old is not None and type(old) != dict:
+ raise error.Error("\"old\" <row> is not object", old)
+ if new is not None and type(new) != dict:
+ raise error.Error("\"new\" <row> is not object", new)
+ if (old is not None) + (new is not None) != len(row_update):
+ raise error.Error("<row-update> contains unexpected "
+ "member", row_update)
+ if not old and not new:
+ raise error.Error("<row-update> missing \"old\" and "
+ "\"new\" members", row_update)
+
+ if self.__parse_row_update(table, uuid, old, new):
+ self.change_seqno += 1
+
+ def __parse_row_update(self, table, uuid, old, new):
+ """Returns True if a column changed, False otherwise."""
+ row = self.data[table.name].get(uuid)
+ if not new:
+ # Delete row.
+ if row:
+ del self.data[table.name][uuid]
+ else:
+ # XXX rate-limit
+ logging.warning("cannot delete missing row %s from table %s"
+ % (uuid, table.name))
+ return False
+ elif not old:
+ # Insert row.
+ if not row:
+ row = self.__create_row(table, uuid)
+ else:
+ # XXX rate-limit
+ logging.warning("cannot add existing row %s to table %s"
+ % (uuid, table.name))
+ self.__modify_row(table, row, new)
+ else:
+ if not row:
+ row = self.__create_row(table, uuid)
+ # XXX rate-limit
+ logging.warning("cannot modify missing row %s in table %s"
+ % (uuid, table_name))
+ self.__modify_row(table, row, new)
+ return True
+
+ def __modify_row(self, table, row, row_json):
+ changed = False
+ for column_name, datum_json in row_json.iteritems():
+ column = table.columns.get(column_name)
+ if not column:
+ # XXX rate-limit
+ logging.warning("unknown column %s updating table %s"
+ % (column_name, table.name))
+ continue
+
+ try:
+ datum = ovs.db.data.Datum.from_json(column.type, datum_json)
+ except error.Error, e:
+ # XXX rate-limit
+ logging.warning("error parsing column %s in table %s: %s"
+ % (column_name, table_name, e))
+ continue
+
+ if datum != row.__dict__[column_name]:
+ row.__dict__[column_name] = datum
+ changed = True
+ else:
+ # Didn't really change but the OVSDB monitor protocol always
+ # includes every value in a row.
+ pass
+ return changed
+
+ def __clear(self):
+ if self.data != {}:
+ for table_name in self.schema.tables:
+ if self.data[table_name] != {}:
+ self.change_seqno += 1
+ break
+
+ self.data = {}
+ for table_name in self.schema.tables:
+ self.data[table_name] = {}
+
+ def __create_row(self, table, uuid):
+ class Row(object):
+ pass
+ row = self.data[table.name][uuid] = Row()
+ for column in table.columns.itervalues():
+ row.__dict__[column.name] = ovs.db.data.Datum.default(column.type)
+ return row
+
+ def force_reconnect(self):
+ """Forces the IDL to drop its connection to the database and reconnect.
+ In the meantime, the contents of the IDL will not change."""
+ self.session.force_reconnect()
diff --git a/python/ovs/db/parser.py b/python/ovs/db/parser.py
new file mode 100644
index 000000000..07ce8e2b0
--- /dev/null
+++ b/python/ovs/db/parser.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+from ovs.db import error
+
+class Parser(object):
+ def __init__(self, json, name):
+ self.name = name
+ self.json = json
+ if type(json) != dict:
+ self.__raise_error("Object expected.")
+ self.used = set()
+
+ def __get(self, name, types, optional, default=None):
+ if name in self.json:
+ self.used.add(name)
+ member = float_to_int(self.json[name])
+ if is_identifier(member) and "id" in types:
+ return member
+ if len(types) and type(member) not in types:
+ self.__raise_error("Type mismatch for member '%s'." % name)
+ return member
+ else:
+ if not optional:
+ self.__raise_error("Required '%s' member is missing." % name)
+ return default
+
+ def get(self, name, types):
+ return self.__get(name, types, False)
+
+ def get_optional(self, name, types, default=None):
+ return self.__get(name, types, True, default)
+
+ def __raise_error(self, message):
+ raise error.Error("Parsing %s failed: %s" % (self.name, message),
+ self.json)
+
+ def finish(self):
+ missing = set(self.json) - set(self.used)
+ if missing:
+ name = missing.pop()
+ if len(missing) > 1:
+ self.__raise_error("Member '%s' and %d other members "
+ "are present but not allowed here"
+ % (name, len(missing)))
+ elif missing:
+ self.__raise_error("Member '%s' and 1 other member "
+ "are present but not allowed here" % name)
+ else:
+ self.__raise_error("Member '%s' is present but not "
+ "allowed here" % name)
+
+def float_to_int(x):
+ # XXX still needed?
+ if type(x) == float:
+ integer = int(x)
+ if integer == x and integer >= -2**53 and integer < 2**53:
+ return integer
+ return x
+
+id_re = re.compile("[_a-zA-Z][_a-zA-Z0-9]*$")
+def is_identifier(s):
+ return type(s) in [str, unicode] and id_re.match(s)
+
+def json_type_to_string(type):
+ if type == None:
+ return "null"
+ elif type == bool:
+ return "boolean"
+ elif type == dict:
+ return "object"
+ elif type == list:
+ return "array"
+ elif type in [int, long, float]:
+ return "number"
+ elif type in [str, unicode]:
+ return "string"
+ else:
+ return "<invalid>"
+
+def unwrap_json(json, name, need_type):
+ if (type(json) != list or len(json) != 2 or json[0] != name or
+ type(json[1]) != need_type):
+ raise error.Error("expected [\"%s\", <%s>]"
+ % (name, json_type_to_string(need_type)), json)
+ return json[1]
+
+def parse_json_pair(json):
+ if type(json) != list or len(json) != 2:
+ raise error.Error("expected 2-element array", json)
+ return json
+
diff --git a/python/ovs/db/schema.py b/python/ovs/db/schema.py
new file mode 100644
index 000000000..189b137c7
--- /dev/null
+++ b/python/ovs/db/schema.py
@@ -0,0 +1,159 @@
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from ovs.db import error
+import ovs.db.parser
+from ovs.db import types
+
+class DbSchema(object):
+ """Schema for an OVSDB database."""
+
+ def __init__(self, name, tables):
+ self.name = name
+ self.tables = tables
+
+ # Validate that all ref_tables refer to the names of tables
+ # that exist.
+ for table in self.tables.itervalues():
+ for column in table.columns.itervalues():
+ self.__check_ref_table(column, column.type.key, "key")
+ self.__check_ref_table(column, column.type.value, "value")
+
+ @staticmethod
+ def from_json(json):
+ parser = ovs.db.parser.Parser(json, "database schema")
+ name = parser.get("name", ['id'])
+ tablesJson = parser.get("tables", [dict])
+ parser.finish()
+
+ tables = {}
+ for tableName, tableJson in tablesJson.iteritems():
+ if tableName.startswith('_'):
+ raise error.Error("names beginning with \"_\" are reserved",
+ json)
+ elif not ovs.db.parser.is_identifier(tableName):
+ raise error.Error("name must be a valid id", json)
+ tables[tableName] = TableSchema.from_json(tableJson, tableName)
+
+ return DbSchema(name, tables)
+
+ def to_json(self):
+ tables = {}
+ for table in self.tables.itervalues():
+ tables[table.name] = table.to_json()
+ return {"name": self.name, "tables": tables}
+
+ def __check_ref_table(self, column, base, base_name):
+ if (base and base.type == types.UuidType and base.ref_table and
+ base.ref_table not in self.tables):
+ raise error.Error("column %s %s refers to undefined table %s"
+ % (column.name, base_name, base.ref_table),
+ tag="syntax error")
+
+class IdlSchema(DbSchema):
+ def __init__(self, name, tables, idlPrefix, idlHeader):
+ DbSchema.__init__(self, name, tables)
+ self.idlPrefix = idlPrefix
+ self.idlHeader = idlHeader
+
+ @staticmethod
+ def from_json(json):
+ parser = ovs.db.parser.Parser(json, "IDL schema")
+ idlPrefix = parser.get("idlPrefix", [unicode])
+ idlHeader = parser.get("idlHeader", [unicode])
+
+ subjson = dict(json)
+ del subjson["idlPrefix"]
+ del subjson["idlHeader"]
+ schema = DbSchema.from_json(subjson)
+
+ return IdlSchema(schema.name, schema.tables, idlPrefix, idlHeader)
+
+class TableSchema(object):
+ def __init__(self, name, columns, mutable=True, max_rows=sys.maxint):
+ self.name = name
+ self.columns = columns
+ self.mutable = mutable
+ self.max_rows = max_rows
+
+ @staticmethod
+ def from_json(json, name):
+ parser = ovs.db.parser.Parser(json, "table schema for table %s" % name)
+ columnsJson = parser.get("columns", [dict])
+ mutable = parser.get_optional("mutable", [bool], True)
+ max_rows = parser.get_optional("maxRows", [int])
+ parser.finish()
+
+ if max_rows == None:
+ max_rows = sys.maxint
+ elif max_rows <= 0:
+ raise error.Error("maxRows must be at least 1", json)
+
+ if not columnsJson:
+ raise error.Error("table must have at least one column", json)
+
+ columns = {}
+ for columnName, columnJson in columnsJson.iteritems():
+ if columnName.startswith('_'):
+ raise error.Error("names beginning with \"_\" are reserved",
+ json)
+ elif not ovs.db.parser.is_identifier(columnName):
+ raise error.Error("name must be a valid id", json)
+ columns[columnName] = ColumnSchema.from_json(columnJson,
+ columnName)
+
+ return TableSchema(name, columns, mutable, max_rows)
+
+ def to_json(self):
+ json = {}
+ if not self.mutable:
+ json["mutable"] = False
+
+ json["columns"] = columns = {}
+ for column in self.columns.itervalues():
+ if not column.name.startswith("_"):
+ columns[column.name] = column.to_json()
+
+ if self.max_rows != sys.maxint:
+ json["maxRows"] = self.max_rows
+
+ return json
+
+class ColumnSchema(object):
+ def __init__(self, name, mutable, persistent, type):
+ self.name = name
+ self.mutable = mutable
+ self.persistent = persistent
+ self.type = type
+
+ @staticmethod
+ def from_json(json, name):
+ parser = ovs.db.parser.Parser(json, "schema for column %s" % name)
+ mutable = parser.get_optional("mutable", [bool], True)
+ ephemeral = parser.get_optional("ephemeral", [bool], False)
+ type = types.Type.from_json(parser.get("type", [dict, unicode]))
+ parser.finish()
+
+ return ColumnSchema(name, mutable, not ephemeral, type)
+
+ def to_json(self):
+ json = {"type": self.type.to_json()}
+ if not self.mutable:
+ json["mutable"] = False
+ if not self.persistent:
+ json["ephemeral"] = True
+ return json
+
diff --git a/python/ovs/db/types.py b/python/ovs/db/types.py
new file mode 100644
index 000000000..aa0a8eda8
--- /dev/null
+++ b/python/ovs/db/types.py
@@ -0,0 +1,545 @@
+# Copyright (c) 2009, 2010 Nicira Networks
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from ovs.db import error
+import ovs.db.parser
+import ovs.db.data
+import ovs.ovsuuid
+
+class AtomicType(object):
+ def __init__(self, name, default):
+ self.name = name
+ self.default = default
+
+ @staticmethod
+ def from_string(s):
+ if s != "void":
+ for atomic_type in ATOMIC_TYPES:
+ if s == atomic_type.name:
+ return atomic_type
+ raise error.Error("\"%s\" is not an atomic type" % s)
+
+ @staticmethod
+ def from_json(json):
+ if type(json) not in [str, unicode]:
+ raise error.Error("atomic-type expected", json)
+ try:
+ return AtomicType.from_string(json)
+ except error.Error:
+ raise error.Error("\"%s\" is not an atomic-type" % json, json)
+
+ def __str__(self):
+ return self.name
+
+ def to_string(self):
+ return self.name
+
+ def to_json(self):
+ return self.name
+
+ def default_atom(self):
+ return ovs.db.data.Atom(self, self.default)
+
+VoidType = AtomicType("void", None)
+IntegerType = AtomicType("integer", 0)
+RealType = AtomicType("real", 0.0)
+BooleanType = AtomicType("boolean", False)
+StringType = AtomicType("string", "")
+UuidType = AtomicType("uuid", ovs.ovsuuid.UUID.zero())
+
+ATOMIC_TYPES = [VoidType, IntegerType, RealType, BooleanType, StringType,
+ UuidType]
+
+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 commafy(x):
+ """Returns integer x formatted in decimal with thousands set off by
+ commas."""
+ return _commafy("%d" % x)
+def _commafy(s):
+ if s.startswith('-'):
+ return '-' + _commafy(s[1:])
+ elif len(s) <= 3:
+ return s
+ else:
+ return _commafy(s[:-3]) + ',' + _commafy(s[-3:])
+
+def returnUnchanged(x):
+ return x
+
+class BaseType(object):
+ def __init__(self, type_, enum=None, min=None, max=None,
+ min_length = 0, max_length=sys.maxint, ref_table=None):
+ assert isinstance(type_, AtomicType)
+ self.type = type_
+ self.enum = enum
+ self.min = min
+ self.max = max
+ self.min_length = min_length
+ self.max_length = max_length
+ self.ref_table = ref_table
+
+ def default(self):
+ return ovs.db.data.Atom.default(self.type)
+
+ def __eq__(self, other):
+ if not isinstance(other, BaseType):
+ return NotImplemented
+ return (self.type == other.type and self.enum == other.enum and
+ self.min == other.min and self.max == other.max and
+ self.min_length == other.min_length and
+ self.max_length == other.max_length and
+ self.ref_table == other.ref_table)
+
+ def __ne__(self, other):
+ if not isinstance(other, BaseType):
+ return NotImplemented
+ else:
+ return not (self == other)
+
+ @staticmethod
+ def __parse_uint(parser, name, default):
+ value = parser.get_optional(name, [int, long])
+ if value is None:
+ value = default
+ else:
+ max_value = 2**32 - 1
+ if value < 0 or value > max_value:
+ raise error.Error("%s out of valid range 0 to %d"
+ % (name, max_value), value)
+ return value
+
+ @staticmethod
+ def from_json(json):
+ if type(json) == unicode:
+ return BaseType(AtomicType.from_json(json))
+
+ parser = ovs.db.parser.Parser(json, "ovsdb type")
+ atomic_type = AtomicType.from_json(parser.get("type", [str, unicode]))
+
+ base = BaseType(atomic_type)
+
+ enum = parser.get_optional("enum", [])
+ if enum is not None:
+ base.enum = ovs.db.data.Datum.from_json(BaseType.get_enum_type(base.type), enum)
+ elif base.type == IntegerType:
+ base.min = parser.get_optional("minInteger", [int, long])
+ base.max = parser.get_optional("maxInteger", [int, long])
+ if base.min is not None and base.max is not None and base.min > base.max:
+ raise error.Error("minInteger exceeds maxInteger", json)
+ elif base.type == RealType:
+ base.min = parser.get_optional("minReal", [int, long, float])
+ base.max = parser.get_optional("maxReal", [int, long, float])
+ if base.min is not None and base.max is not None and base.min > base.max:
+ raise error.Error("minReal exceeds maxReal", json)
+ elif base.type == StringType:
+ base.min_length = BaseType.__parse_uint(parser, "minLength", 0)
+ base.max_length = BaseType.__parse_uint(parser, "maxLength",
+ sys.maxint)
+ if base.min_length > base.max_length:
+ raise error.Error("minLength exceeds maxLength", json)
+ elif base.type == UuidType:
+ base.ref_table = parser.get_optional("refTable", ['id'])
+ if base.ref_table:
+ base.ref_type = parser.get_optional("refType", [str, unicode],
+ "strong")
+ if base.ref_type not in ['strong', 'weak']:
+ raise error.Error("refType must be \"strong\" or \"weak\" "
+ "(not \"%s\")" % base.ref_type)
+ parser.finish()
+
+ return base
+
+ def to_json(self):
+ if not self.has_constraints():
+ return self.type.to_json()
+
+ json = {'type': self.type.to_json()}
+
+ if self.enum:
+ json['enum'] = self.enum.to_json()
+
+ if self.type == IntegerType:
+ if self.min is not None:
+ json['minInteger'] = self.min
+ if self.max is not None:
+ json['maxInteger'] = self.max
+ elif self.type == RealType:
+ if self.min is not None:
+ json['minReal'] = self.min
+ if self.max is not None:
+ json['maxReal'] = self.max
+ elif self.type == StringType:
+ if self.min_length != 0:
+ json['minLength'] = self.min_length
+ if self.max_length != sys.maxint:
+ json['maxLength'] = self.max_length
+ elif self.type == UuidType:
+ if self.ref_table:
+ json['refTable'] = self.ref_table
+ if self.ref_type != 'strong':
+ json['refType'] = self.ref_type
+ return json
+
+ def clone(self):
+ return BaseType(self.type, self.enum.clone(), self.min, self.max,
+ self.min_length, self.max_length, self.ref_table)
+
+ def is_valid(self):
+ if self.type in (VoidType, BooleanType, UuidType):
+ return True
+ elif self.type in (IntegerType, RealType):
+ return self.min is None or self.max is None or self.min <= self.max
+ elif self.type == StringType:
+ return self.min_length <= self.max_length
+ else:
+ return False
+
+ def has_constraints(self):
+ return (self.enum is not None or self.min is not None or self.max is not None or
+ self.min_length != 0 or self.max_length != sys.maxint or
+ self.ref_table is not None)
+
+ def without_constraints(self):
+ return BaseType(self.type)
+
+ @staticmethod
+ def get_enum_type(atomic_type):
+ """Returns the type of the 'enum' member for a BaseType whose
+ 'type' is 'atomic_type'."""
+ return Type(BaseType(atomic_type), None, 1, sys.maxint)
+
+ def is_ref(self):
+ return self.type == UuidType and self.ref_table is not None
+
+ def is_strong_ref(self):
+ return self.is_ref() and self.ref_type == 'strong'
+
+ def is_weak_ref(self):
+ return self.is_ref() and self.ref_type == 'weak'
+
+ def toEnglish(self, escapeLiteral=returnUnchanged):
+ if self.type == UuidType and self.ref_table:
+ s = escapeLiteral(self.ref_table)
+ if self.ref_type == 'weak':
+ s = "weak reference to " + s
+ return s
+ else:
+ return self.type.to_string()
+
+ 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.min is not None and self.max is not None:
+ if self.type == IntegerType:
+ return 'in range %s to %s' % (commafy(self.min),
+ commafy(self.max))
+ else:
+ return 'in range %g to %g' % (self.min, self.max)
+ elif self.min is not None:
+ if self.type == IntegerType:
+ return 'at least %s' % commafy(self.min)
+ else:
+ return 'at least %g' % self.min
+ elif self.max is not None:
+ if self.type == IntegerType:
+ return 'at most %s' % commafy(self.max)
+ else:
+ return 'at most %g' % self.max
+ elif self.min_length is not None and self.max_length is not None:
+ if self.min_length == self.max_length:
+ return 'exactly %d characters long' % (self.min_length)
+ else:
+ return 'between %d and %d characters long' % (self.min_length, self.max_length)
+ elif self.min_length is not None:
+ return 'at least %d characters long' % self.min_length
+ elif self.max_length is not None:
+ return 'at most %d characters long' % self.max_length
+ else:
+ return ''
+
+ def toCType(self, prefix):
+ if self.ref_table:
+ return "struct %s%s *" % (prefix, self.ref_table.lower())
+ else:
+ return {IntegerType: 'int64_t ',
+ RealType: 'double ',
+ UuidType: 'struct uuid ',
+ BooleanType: 'bool ',
+ StringType: 'char *'}[self.type]
+
+ def toAtomicType(self):
+ return "OVSDB_TYPE_%s" % self.type.to_string().upper()
+
+ def copyCValue(self, dst, src):
+ args = {'dst': dst, 'src': src}
+ if self.ref_table:
+ return ("%(dst)s = %(src)s->header_.uuid;") % args
+ elif self.type == StringType:
+ return "%(dst)s = xstrdup(%(src)s);" % args
+ else:
+ return "%(dst)s = %(src)s;" % args
+
+ def initCDefault(self, var, is_optional):
+ if self.ref_table:
+ return "%s = NULL;" % var
+ elif self.type == StringType and not is_optional:
+ return "%s = \"\";" % var
+ else:
+ pattern = {IntegerType: '%s = 0;',
+ RealType: '%s = 0.0;',
+ UuidType: 'uuid_zero(&%s);',
+ BooleanType: '%s = false;',
+ StringType: '%s = NULL;'}[self.type]
+ return pattern % var
+
+ def cInitBaseType(self, indent, var):
+ stmts = []
+ stmts.append('ovsdb_base_type_init(&%s, OVSDB_TYPE_%s);' % (
+ var, self.type.to_string().upper()),)
+ if self.enum:
+ stmts.append("%s.enum_ = xmalloc(sizeof *%s.enum_);"
+ % (var, var))
+ stmts += self.enum.cInitDatum("%s.enum_" % var)
+ if self.type == IntegerType:
+ if self.min is not None:
+ stmts.append('%s.u.integer.min = INT64_C(%d);' % (var, self.min))
+ if self.max is not None:
+ stmts.append('%s.u.integer.max = INT64_C(%d);' % (var, self.max))
+ elif self.type == RealType:
+ if self.min is not None:
+ stmts.append('%s.u.real.min = %d;' % (var, self.min))
+ if self.max is not None:
+ stmts.append('%s.u.real.max = %d;' % (var, self.max))
+ elif self.type == StringType:
+ if self.min_length is not None:
+ stmts.append('%s.u.string.minLen = %d;' % (var, self.min_length))
+ if self.max_length is not None:
+ stmts.append('%s.u.string.maxLen = %d;' % (var, self.max_length))
+ elif self.type == UuidType:
+ if self.ref_table is not None:
+ stmts.append('%s.u.uuid.refTableName = "%s";' % (var, escapeCString(self.ref_table)))
+ return '\n'.join([indent + stmt for stmt in stmts])
+
+class Type(object):
+ def __init__(self, key, value=None, n_min=1, n_max=1):
+ self.key = key
+ self.value = value
+ self.n_min = n_min
+ self.n_max = n_max
+
+ def clone(self):
+ if self.value is None:
+ value = None
+ else:
+ value = self.value.clone()
+ return Type(self.key.clone(), value, self.n_min, self.n_max)
+
+ def __eq__(self, other):
+ if not isinstance(other, Type):
+ return NotImplemented
+ return (self.key == other.key and self.value == other.value and
+ self.n_min == other.n_min and self.n_max == other.n_max)
+
+ def __ne__(self, other):
+ if not isinstance(other, BaseType):
+ return NotImplemented
+ else:
+ return not (self == other)
+
+ def is_valid(self):
+ return (self.key.type != VoidType and self.key.is_valid() and
+ (self.value is None or
+ (self.value.type != VoidType and self.value.is_valid())) and
+ self.n_min <= 1 and
+ self.n_min <= self.n_max and
+ self.n_max >= 1)
+
+ def is_scalar(self):
+ return self.n_min == 1 and self.n_max == 1 and not self.value
+
+ def is_optional(self):
+ return self.n_min == 0 and self.n_max == 1
+
+ def is_composite(self):
+ return self.n_max > 1
+
+ def is_set(self):
+ return self.value is None and (self.n_min != 1 or self.n_max != 1)
+
+ def is_map(self):
+ return self.value is not None
+
+ def is_optional_pointer(self):
+ return (self.is_optional() and not self.value
+ and (self.key.type == StringType or self.key.ref_table))
+
+ @staticmethod
+ def __n_from_json(json, default):
+ if json is None:
+ return default
+ elif type(json) == int and json >= 0 and json <= sys.maxint:
+ return json
+ else:
+ raise error.Error("bad min or max value", json)
+
+ @staticmethod
+ def from_json(json):
+ if type(json) in [str, unicode]:
+ return Type(BaseType.from_json(json))
+
+ parser = ovs.db.parser.Parser(json, "ovsdb type")
+ key_json = parser.get("key", [dict, unicode])
+ value_json = parser.get_optional("value", [dict, unicode])
+ min_json = parser.get_optional("min", [int])
+ max_json = parser.get_optional("max", [int, str, unicode])
+ parser.finish()
+
+ key = BaseType.from_json(key_json)
+ if value_json:
+ value = BaseType.from_json(value_json)
+ else:
+ value = None
+
+ n_min = Type.__n_from_json(min_json, 1)
+
+ if max_json == 'unlimited':
+ n_max = sys.maxint
+ else:
+ n_max = Type.__n_from_json(max_json, 1)
+
+ type_ = Type(key, value, n_min, n_max)
+ if not type_.is_valid():
+ raise error.Error("ovsdb type fails constraint checks", json)
+ return type_
+
+ def to_json(self):
+ if self.is_scalar() and not self.key.has_constraints():
+ return self.key.to_json()
+
+ json = {"key": self.key.to_json()}
+ if self.value is not None:
+ json["value"] = self.value.to_json()
+ if self.n_min != 1:
+ json["min"] = self.n_min
+ if self.n_max == sys.maxint:
+ json["max"] = "unlimited"
+ elif self.n_max != 1:
+ json["max"] = self.n_max
+ return json
+
+ def toEnglish(self, escapeLiteral=returnUnchanged):
+ keyName = self.key.toEnglish(escapeLiteral)
+ if self.value:
+ valueName = self.value.toEnglish(escapeLiteral)
+
+ if self.is_scalar():
+ return keyName
+ elif self.is_optional():
+ if self.value:
+ return "optional %s-%s pair" % (keyName, valueName)
+ else:
+ return "optional %s" % keyName
+ else:
+ if self.n_max == sys.maxint:
+ if self.n_min:
+ quantity = "%d or more " % self.n_min
+ else:
+ quantity = ""
+ elif self.n_min:
+ quantity = "%d to %d " % (self.n_min, self.n_max)
+ else:
+ quantity = "up to %d " % self.n_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.n_min == 1 and self.n_max == 1 and self.key.type == StringType:
+ 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.n_min)
+ if self.n_max == sys.maxint:
+ max = "UINT_MAX"
+ else:
+ max = self.n_max
+ initMax = "%s%s.n_max = %s;" % (indent, var, max)
+ return "\n".join((initKey, initValue, initMin, initMax))
+