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