ovs.db.data: Make Datum.check_constraints() work.
[sliver-openvswitch.git] / python / ovs / db / data.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 uuid
17
18 import ovs.poller
19 import ovs.socket_util
20 import ovs.json
21 import ovs.jsonrpc
22 import ovs.ovsuuid
23
24 import ovs.db.parser
25 from ovs.db import error
26 import ovs.db.types
27
28 class ConstraintViolation(error.Error):
29     def __init__(self, msg, json=None):
30         error.Error.__init__(self, msg, json, tag="constraint violation")
31
32 def escapeCString(src):
33     dst = []
34     for c in src:
35         if c in "\\\"":
36             dst.append("\\" + c)
37         elif ord(c) < 32:
38             if c == '\n':
39                 dst.append('\\n')
40             elif c == '\r':
41                 dst.append('\\r')
42             elif c == '\a':
43                 dst.append('\\a')
44             elif c == '\b':
45                 dst.append('\\b')
46             elif c == '\f':
47                 dst.append('\\f')
48             elif c == '\t':
49                 dst.append('\\t')
50             elif c == '\v':
51                 dst.append('\\v')
52             else:
53                 dst.append('\\%03o' % ord(c))
54         else:
55             dst.append(c)
56     return ''.join(dst)
57
58 def returnUnchanged(x):
59     return x
60
61 class Atom(object):
62     def __init__(self, type_, value=None):
63         self.type = type_
64         if value is not None:
65             self.value = value
66         else:
67             self.value = type_.default_atom()
68
69     def __cmp__(self, other):
70         if not isinstance(other, Atom) or self.type != other.type:
71             return NotImplemented
72         elif self.value < other.value:
73             return -1
74         elif self.value > other.value:
75             return 1
76         else:
77             return 0
78
79     def __hash__(self):
80         return hash(self.value)
81
82     @staticmethod
83     def default(type_):
84         return Atom(type_)
85
86     def is_default(self):
87         return self == self.default(self.type)
88
89     @staticmethod
90     def from_json(base, json, symtab=None):
91         type_ = base.type
92         json = ovs.db.parser.float_to_int(json)
93         if ((type_ == ovs.db.types.IntegerType and type(json) in [int, long])
94             or (type_ == ovs.db.types.RealType and type(json) in [int, long, float])
95             or (type_ == ovs.db.types.BooleanType and type(json) == bool)
96             or (type_ == ovs.db.types.StringType and type(json) in [str, unicode])):
97             atom = Atom(type_, json)
98         elif type_ == ovs.db.types.UuidType:
99             atom = Atom(type_, ovs.ovsuuid.UUID.from_json(json, symtab))
100         else:
101             raise error.Error("expected %s" % type_.to_string(), json)
102         atom.check_constraints(base)
103         return atom
104
105     def check_constraints(self, base):
106         """Checks whether 'atom' meets the constraints (if any) defined in
107         'base' and raises an ovs.db.error.Error if any constraint is violated.
108
109         'base' and 'atom' must have the same type.
110
111         Checking UUID constraints is deferred to transaction commit time, so
112         this function does nothing for UUID constraints."""
113         assert base.type == self.type
114         if base.enum is not None and self not in base.enum:
115             raise ConstraintViolation(
116                 "%s is not one of the allowed values (%s)"
117                 % (self.to_string(), base.enum.to_string()))
118         elif base.type in [ovs.db.types.IntegerType, ovs.db.types.RealType]:
119             if ((base.min is None or self.value >= base.min) and
120                 (base.max is None or self.value <= base.max)):
121                 pass
122             elif base.min is not None and base.max is not None:
123                 raise ConstraintViolation(
124                     "%s is not in the valid range %.15g to %.15g (inclusive)"
125                     % (self.to_string(), base.min, base.max))
126             elif base.min is not None:
127                 raise ConstraintViolation(
128                     "%s is less than minimum allowed value %.15g"
129                             % (self.to_string(), base.min))
130             else:
131                 raise ConstraintViolation(
132                     "%s is greater than maximum allowed value %.15g"
133                     % (self.to_string(), base.max))
134         elif base.type == ovs.db.types.StringType:
135             # XXX The C version validates that the string is valid UTF-8 here.
136             # Do we need to do that in Python too?
137             s = self.value
138             length = len(s)
139             if length < base.min_length:
140                 raise ConstraintViolation(
141                     '"%s" length %d is less than minimum allowed length %d'
142                     % (s, length, base.min_length))
143             elif length > base.max_length:
144                 raise ConstraintViolation(
145                     '"%s" length %d is greater than maximum allowed '
146                     'length %d' % (s, length, base.max_length))
147     
148     def to_json(self):
149         if self.type == ovs.db.types.UuidType:
150             return self.value.to_json()
151         else:
152             return self.value
153
154     def cInitAtom(self, var):
155         if self.type == ovs.db.types.IntegerType:
156             return ['%s.integer = %d;' % (var, self.value)]
157         elif self.type == ovs.db.types.RealType:
158             return ['%s.real = %.15g;' % (var, self.value)]
159         elif self.type == ovs.db.types.BooleanType:
160             if self.value:
161                 return ['%s.boolean = true;']
162             else:
163                 return ['%s.boolean = false;']
164         elif self.type == ovs.db.types.StringType:
165             return ['%s.string = xstrdup("%s");'
166                     % (var, escapeCString(self.value))]
167         elif self.type == ovs.db.types.UuidType:
168             return self.value.cInitUUID(var)
169
170     def toEnglish(self, escapeLiteral=returnUnchanged):
171         if self.type == ovs.db.types.IntegerType:
172             return '%d' % self.value
173         elif self.type == ovs.db.types.RealType:
174             return '%.15g' % self.value
175         elif self.type == ovs.db.types.BooleanType:
176             if self.value:
177                 return 'true'
178             else:
179                 return 'false'
180         elif self.type == ovs.db.types.StringType:
181             return escapeLiteral(self.value)
182         elif self.type == ovs.db.types.UuidType:
183             return self.value.value
184
185     __need_quotes_re = re.compile("$|true|false|[^_a-zA-Z]|.*[^-._a-zA-Z]")
186     @staticmethod
187     def __string_needs_quotes(s):
188         return Atom.__need_quotes_re.match(s)
189
190     def to_string(self):
191         if self.type == ovs.db.types.IntegerType:
192             return '%d' % self.value
193         elif self.type == ovs.db.types.RealType:
194             return '%.15g' % self.value
195         elif self.type == ovs.db.types.BooleanType:
196             if self.value:
197                 return 'true'
198             else:
199                 return 'false'
200         elif self.type == ovs.db.types.StringType:
201             if Atom.__string_needs_quotes(self.value):
202                 return ovs.json.to_string(self.value)
203             else:
204                 return self.value
205         elif self.type == ovs.db.types.UuidType:
206             return str(self.value)
207
208     @staticmethod
209     def new(x):
210         if type(x) in [int, long]:
211             t = ovs.db.types.IntegerType
212         elif type(x) == float:
213             t = ovs.db.types.RealType
214         elif x in [False, True]:
215             t = ovs.db.types.BooleanType
216         elif type(x) in [str, unicode]:
217             t = ovs.db.types.StringType
218         elif isinstance(x, uuid):
219             t = ovs.db.types.UuidType
220         else:
221             raise TypeError
222         return Atom(t, x)
223
224 class Datum(object):
225     def __init__(self, type_, values={}):
226         self.type = type_
227         self.values = values
228
229     def __cmp__(self, other):
230         if not isinstance(other, Datum):
231             return NotImplemented
232         elif self.values < other.values:
233             return -1
234         elif self.values > other.values:
235             return 1
236         else:
237             return 0
238
239     __hash__ = None
240
241     def __contains__(self, item):
242         return item in self.values
243
244     def clone(self):
245         return Datum(self.type, dict(self.values))
246
247     @staticmethod
248     def default(type_):
249         if type_.n_min == 0:
250             values = {}
251         elif type_.is_map():
252             values = {type_.key.default(): type_.value.default()}
253         else:
254             values = {type_.key.default(): None}
255         return Datum(type_, values)
256
257     def is_default(self):
258         return self == Datum.default(self.type)
259
260     def check_constraints(self):
261         """Checks that each of the atoms in 'datum' conforms to the constraints
262         specified by its 'type' and raises an ovs.db.error.Error.
263
264         This function is not commonly useful because the most ordinary way to
265         obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
266         which check constraints themselves."""
267         for keyAtom, valueAtom in self.values.iteritems():
268             keyAtom.check_constraints(self.type.key)
269             if valueAtom is not None:
270                 valueAtom.check_constraints(self.type.value)
271
272     @staticmethod
273     def from_json(type_, json, symtab=None):
274         """Parses 'json' as a datum of the type described by 'type'.  If
275         successful, returns a new datum.  On failure, raises an
276         ovs.db.error.Error.
277         
278         Violations of constraints expressed by 'type' are treated as errors.
279         
280         If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
281         Refer to ovsdb/SPECS for information about this, and for the syntax
282         that this function accepts."""
283         is_map = type_.is_map()
284         if (is_map or
285             (type(json) == list and len(json) > 0 and json[0] == "set")):
286             if is_map:
287                 class_ = "map"
288             else:
289                 class_ = "set"
290
291             inner = ovs.db.parser.unwrap_json(json, class_, list)
292             n = len(inner)
293             if n < type_.n_min or n > type_.n_max:
294                 raise error.Error("%s must have %d to %d members but %d are "
295                                   "present" % (class_, type_.n_min,
296                                                type_.n_max, n),
297                                   json)
298
299             values = {}
300             for element in inner:
301                 if is_map:
302                     key, value = ovs.db.parser.parse_json_pair(element)
303                     keyAtom = Atom.from_json(type_.key, key, symtab)
304                     valueAtom = Atom.from_json(type_.value, value, symtab)
305                 else:
306                     keyAtom = Atom.from_json(type_.key, element, symtab)
307                     valueAtom = None
308
309                 if keyAtom in values:
310                     if is_map:
311                         raise error.Error("map contains duplicate key")
312                     else:
313                         raise error.Error("set contains duplicate")
314
315                 values[keyAtom] = valueAtom
316
317             return Datum(type_, values)
318         else:
319             keyAtom = Atom.from_json(type_.key, json, symtab)
320             return Datum(type_, {keyAtom: None})
321
322     def to_json(self):
323         if self.type.is_map():
324             return ["map", [[k.to_json(), v.to_json()]
325                             for k, v in sorted(self.values.items())]]
326         elif len(self.values) == 1:
327             key = self.values.keys()[0]
328             return key.to_json()
329         else:
330             return ["set", [k.to_json() for k in sorted(self.values.keys())]]
331
332     def to_string(self):
333         head = tail = None
334         if self.type.n_max > 1 or len(self.values) == 0:
335             if self.type.is_map():
336                 head = "{"
337                 tail = "}"
338             else:
339                 head = "["
340                 tail = "]"
341
342         s = []
343         if head:
344             s.append(head)
345
346         for i, key in enumerate(sorted(self.values)):
347             if i:
348                 s.append(", ")
349
350             s.append(key.to_string())
351             if self.type.is_map():
352                 s.append("=")
353                 s.append(self.values[key].to_string())
354
355         if tail:
356             s.append(tail)
357         return ''.join(s)
358
359     def as_list(self):
360         if self.type.is_map():
361             return [[k.value, v.value] for k, v in self.values.iteritems()]
362         else:
363             return [k.value for k in self.values.iterkeys()]
364         
365     def as_scalar(self):
366         if len(self.values) == 1:
367             if self.type.is_map():
368                 k, v = self.values.iteritems()[0]
369                 return [k.value, v.value]
370             else:
371                 return self.values.keys()[0].value
372         else:
373             return None
374
375     def __getitem__(self, key):
376         if not isinstance(key, Atom):
377             key = Atom.new(key)
378         if not self.type.is_map():
379             raise IndexError
380         elif key not in self.values:
381             raise KeyError
382         else:
383             return self.values[key].value
384
385     def get(self, key, default=None):
386         if not isinstance(key, Atom):
387             key = Atom.new(key)
388         if key in self.values:
389             return self.values[key].value
390         else:
391             return default
392         
393     def __str__(self):
394         return self.to_string()
395
396     def conforms_to_type(self):
397         n = len(self.values)
398         return self.type.n_min <= n <= self.type.n_max
399
400     def cInitDatum(self, var):
401         if len(self.values) == 0:
402             return ["ovsdb_datum_init_empty(%s);" % var]
403
404         s = ["%s->n = %d;" % (var, len(self.values))]
405         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
406               % (var, len(self.values), var)]
407
408         for i, key in enumerate(sorted(self.values)):
409             s += key.cInitAtom("%s->keys[%d]" % (var, i))
410         
411         if self.type.value:
412             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
413                   % (var, len(self.values), var)]
414             for i, (key, value) in enumerate(sorted(self.values.items())):
415                 s += value.cInitAtom("%s->values[%d]" % (var, i))
416         else:
417             s += ["%s->values = NULL;" % var]
418
419         if len(self.values) > 1:
420             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
421                   % (var, self.type.key.type.to_string().upper())]
422
423         return s