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