python: Implement write support in Python IDL for OVSDB.
[sliver-openvswitch.git] / python / ovs / db / schema.py
1 # Copyright (c) 2009, 2010, 2011 Nicira Networks
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import re
16 import sys
17
18 from ovs.db import error
19 import ovs.db.parser
20 from ovs.db import types
21
22 def _check_id(name, json):
23     if name.startswith('_'):
24         raise error.Error('names beginning with "_" are reserved', json)
25     elif not ovs.db.parser.is_identifier(name):
26         raise error.Error("name must be a valid id", json)
27
28 class DbSchema(object):
29     """Schema for an OVSDB database."""
30
31     def __init__(self, name, version, tables):
32         self.name = name
33         self.version = version
34         self.tables = tables
35
36         # "isRoot" was not part of the original schema definition.  Before it
37         # was added, there was no support for garbage collection.  So, for
38         # backward compatibility, if the root set is empty then assume that
39         # every table is in the root set.
40         if self.__root_set_size() == 0:
41             for table in self.tables.itervalues():
42                 table.is_root = True
43
44         # Find the "ref_table"s referenced by "ref_table_name"s.
45         #
46         # Also force certain columns to be persistent, as explained in
47         # __check_ref_table().  This requires 'is_root' to be known, so this
48         # must follow the loop updating 'is_root' above.
49         for table in self.tables.itervalues():
50             for column in table.columns.itervalues():
51                 self.__follow_ref_table(column, column.type.key, "key")
52                 self.__follow_ref_table(column, column.type.value, "value")
53
54     def __root_set_size(self):
55         """Returns the number of tables in the schema's root set."""
56         n_root = 0
57         for table in self.tables.itervalues():
58             if table.is_root:
59                 n_root += 1
60         return n_root
61
62     @staticmethod
63     def from_json(json):
64         parser = ovs.db.parser.Parser(json, "database schema")
65         name = parser.get("name", ['id'])
66         version = parser.get_optional("version", [str, unicode])
67         parser.get_optional("cksum", [str, unicode])
68         tablesJson = parser.get("tables", [dict])
69         parser.finish()
70
71         if (version is not None and
72             not re.match('[0-9]+\.[0-9]+\.[0-9]+$', version)):
73             raise error.Error('schema version "%s" not in format x.y.z'
74                               % version)
75
76         tables = {}
77         for tableName, tableJson in tablesJson.iteritems():
78             _check_id(tableName, json)
79             tables[tableName] = TableSchema.from_json(tableJson, tableName)
80
81         return DbSchema(name, version, tables)
82
83     def to_json(self):
84         # "isRoot" was not part of the original schema definition.  Before it
85         # was added, there was no support for garbage collection.  So, for
86         # backward compatibility, if every table is in the root set then do not
87         # output "isRoot" in table schemas.
88         default_is_root = self.__root_set_size() == len(self.tables)
89
90         tables = {}
91         for table in self.tables.itervalues():
92             tables[table.name] = table.to_json(default_is_root)
93         json = {"name": self.name, "tables": tables}
94         if self.version:
95             json["version"] = self.version
96         return json
97
98     def copy(self):
99         return DbSchema.from_json(self.to_json())
100
101     def __follow_ref_table(self, column, base, base_name):
102         if not base or base.type != types.UuidType or not base.ref_table_name:
103             return
104
105         base.ref_table = self.tables.get(base.ref_table_name)
106         if not base.ref_table:
107             raise error.Error("column %s %s refers to undefined table %s"
108                               % (column.name, base_name, base.ref_table_name),
109                               tag="syntax error")
110
111         if base.is_strong_ref() and not base.ref_table.is_root:
112             # We cannot allow a strong reference to a non-root table to be
113             # ephemeral: if it is the only reference to a row, then replaying
114             # the database log from disk will cause the referenced row to be
115             # deleted, even though it did exist in memory.  If there are
116             # references to that row later in the log (to modify it, to delete
117             # it, or just to point to it), then this will yield a transaction
118             # error.
119             column.persistent = True
120
121 class IdlSchema(DbSchema):
122     def __init__(self, name, version, tables, idlPrefix, idlHeader):
123         DbSchema.__init__(self, name, version, tables)
124         self.idlPrefix = idlPrefix
125         self.idlHeader = idlHeader
126
127     @staticmethod
128     def from_json(json):
129         parser = ovs.db.parser.Parser(json, "IDL schema")
130         idlPrefix = parser.get("idlPrefix", [str, unicode])
131         idlHeader = parser.get("idlHeader", [str, unicode])
132
133         subjson = dict(json)
134         del subjson["idlPrefix"]
135         del subjson["idlHeader"]
136         schema = DbSchema.from_json(subjson)
137
138         return IdlSchema(schema.name, schema.version, schema.tables,
139                          idlPrefix, idlHeader)
140
141 def column_set_from_json(json, columns):
142     if json is None:
143         return tuple(columns)
144     elif type(json) != list:
145         raise error.Error("array of distinct column names expected", json)
146     else:
147         for column_name in json:
148             if type(column_name) not in [str, unicode]:
149                 raise error.Error("array of distinct column names expected",
150                                   json)
151             elif column_name not in columns:
152                 raise error.Error("%s is not a valid column name"
153                                   % column_name, json)
154         if len(set(json)) != len(json):
155             # Duplicate.
156             raise error.Error("array of distinct column names expected", json)
157         return tuple([columns[column_name] for column_name in json])
158
159 class TableSchema(object):
160     def __init__(self, name, columns, mutable=True, max_rows=sys.maxint,
161                  is_root=True, indexes=[]):
162         self.name = name
163         self.columns = columns
164         self.mutable = mutable
165         self.max_rows = max_rows
166         self.is_root = is_root
167         self.indexes = indexes
168
169     @staticmethod
170     def from_json(json, name):
171         parser = ovs.db.parser.Parser(json, "table schema for table %s" % name)
172         columns_json = parser.get("columns", [dict])
173         mutable = parser.get_optional("mutable", [bool], True)
174         max_rows = parser.get_optional("maxRows", [int])
175         is_root = parser.get_optional("isRoot", [bool], False)
176         indexes_json = parser.get_optional("indexes", [list], [])
177         parser.finish()
178
179         if max_rows == None:
180             max_rows = sys.maxint
181         elif max_rows <= 0:
182             raise error.Error("maxRows must be at least 1", json)
183
184         if not columns_json:
185             raise error.Error("table must have at least one column", json)
186
187         columns = {}
188         for column_name, column_json in columns_json.iteritems():
189             _check_id(column_name, json)
190             columns[column_name] = ColumnSchema.from_json(column_json,
191                                                           column_name)
192
193         indexes = []
194         for index_json in indexes_json:
195             index = column_set_from_json(index_json, columns)
196             if not index:
197                 raise error.Error("index must have at least one column", json)
198             elif len(index) == 1:
199                 index[0].unique = True
200             for column in index:
201                 if not column.persistent:
202                     raise error.Error("ephemeral columns (such as %s) may "
203                                       "not be indexed" % column.name, json)
204             indexes.append(index)
205
206         return TableSchema(name, columns, mutable, max_rows, is_root, indexes)
207
208     def to_json(self, default_is_root=False):
209         """Returns this table schema serialized into JSON.
210
211         The "isRoot" member is included in the JSON only if its value would
212         differ from 'default_is_root'.  Ordinarily 'default_is_root' should be
213         false, because ordinarily a table would be not be part of the root set
214         if its "isRoot" member is omitted.  However, garbage collection was not
215         orginally included in OVSDB, so in older schemas that do not include
216         any "isRoot" members, every table is implicitly part of the root set.
217         To serialize such a schema in a way that can be read by older OVSDB
218         tools, specify 'default_is_root' as True.
219         """
220         json = {}
221         if not self.mutable:
222             json["mutable"] = False
223         if default_is_root != self.is_root:
224             json["isRoot"] = self.is_root
225
226         json["columns"] = columns = {}
227         for column in self.columns.itervalues():
228             if not column.name.startswith("_"):
229                 columns[column.name] = column.to_json()
230
231         if self.max_rows != sys.maxint:
232             json["maxRows"] = self.max_rows
233
234         if self.indexes:
235             json["indexes"] = []
236             for index in self.indexes:
237                 json["indexes"].append([column.name for column in index])
238
239         return json
240
241 class ColumnSchema(object):
242     def __init__(self, name, mutable, persistent, type_):
243         self.name = name
244         self.mutable = mutable
245         self.persistent = persistent
246         self.type = type_
247         self.unique = False
248
249     @staticmethod
250     def from_json(json, name):
251         parser = ovs.db.parser.Parser(json, "schema for column %s" % name)
252         mutable = parser.get_optional("mutable", [bool], True)
253         ephemeral = parser.get_optional("ephemeral", [bool], False)
254         type_ = types.Type.from_json(parser.get("type", [dict, str, unicode]))
255         parser.finish()
256
257         return ColumnSchema(name, mutable, not ephemeral, type_)
258
259     def to_json(self):
260         json = {"type": self.type.to_json()}
261         if not self.mutable:
262             json["mutable"] = False
263         if not self.persistent:
264             json["ephemeral"] = True
265         return json
266