python: Implement write support in Python IDL for OVSDB.
[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         """Returns the default value for the given type_, which must be an
85         instance of ovs.db.types.AtomicType.
86
87         The default value for each atomic type is;
88
89           - 0, for integer or real atoms.
90
91           - False, for a boolean atom.
92
93           - "", for a string atom.
94
95           - The all-zeros UUID, for a UUID atom."""
96         return Atom(type_)
97
98     def is_default(self):
99         return self == self.default(self.type)
100
101     @staticmethod
102     def from_json(base, json, symtab=None):
103         type_ = base.type
104         json = ovs.db.parser.float_to_int(json)
105         if ((type_ == ovs.db.types.IntegerType and type(json) in [int, long])
106             or (type_ == ovs.db.types.RealType and type(json) in [int, long, float])
107             or (type_ == ovs.db.types.BooleanType and type(json) == bool)
108             or (type_ == ovs.db.types.StringType and type(json) in [str, unicode])):
109             atom = Atom(type_, json)
110         elif type_ == ovs.db.types.UuidType:
111             atom = Atom(type_, ovs.ovsuuid.from_json(json, symtab))
112         else:
113             raise error.Error("expected %s" % type_.to_string(), json)
114         atom.check_constraints(base)
115         return atom
116
117     @staticmethod
118     def from_python(base, value):
119         value = ovs.db.parser.float_to_int(value)
120         if type(value) in base.type.python_types:
121             atom = Atom(base.type, value)
122         else:
123             raise error.Error("expected %s, got %s" % (base.type, type(value)))
124         atom.check_constraints(base)
125         return atom
126
127     def check_constraints(self, base):
128         """Checks whether 'atom' meets the constraints (if any) defined in
129         'base' and raises an ovs.db.error.Error if any constraint is violated.
130
131         'base' and 'atom' must have the same type.
132         Checking UUID constraints is deferred to transaction commit time, so
133         this function does nothing for UUID constraints."""
134         assert base.type == self.type
135         if base.enum is not None and self not in base.enum:
136             raise ConstraintViolation(
137                 "%s is not one of the allowed values (%s)"
138                 % (self.to_string(), base.enum.to_string()))
139         elif base.type in [ovs.db.types.IntegerType, ovs.db.types.RealType]:
140             if ((base.min is None or self.value >= base.min) and
141                 (base.max is None or self.value <= base.max)):
142                 pass
143             elif base.min is not None and base.max is not None:
144                 raise ConstraintViolation(
145                     "%s is not in the valid range %.15g to %.15g (inclusive)"
146                     % (self.to_string(), base.min, base.max))
147             elif base.min is not None:
148                 raise ConstraintViolation(
149                     "%s is less than minimum allowed value %.15g"
150                             % (self.to_string(), base.min))
151             else:
152                 raise ConstraintViolation(
153                     "%s is greater than maximum allowed value %.15g"
154                     % (self.to_string(), base.max))
155         elif base.type == ovs.db.types.StringType:
156             # XXX The C version validates that the string is valid UTF-8 here.
157             # Do we need to do that in Python too?
158             s = self.value
159             length = len(s)
160             if length < base.min_length:
161                 raise ConstraintViolation(
162                     '"%s" length %d is less than minimum allowed length %d'
163                     % (s, length, base.min_length))
164             elif length > base.max_length:
165                 raise ConstraintViolation(
166                     '"%s" length %d is greater than maximum allowed '
167                     'length %d' % (s, length, base.max_length))
168     
169     def to_json(self):
170         if self.type == ovs.db.types.UuidType:
171             return ovs.ovsuuid.to_json(self.value)
172         else:
173             return self.value
174
175     def cInitAtom(self, var):
176         if self.type == ovs.db.types.IntegerType:
177             return ['%s.integer = %d;' % (var, self.value)]
178         elif self.type == ovs.db.types.RealType:
179             return ['%s.real = %.15g;' % (var, self.value)]
180         elif self.type == ovs.db.types.BooleanType:
181             if self.value:
182                 return ['%s.boolean = true;']
183             else:
184                 return ['%s.boolean = false;']
185         elif self.type == ovs.db.types.StringType:
186             return ['%s.string = xstrdup("%s");'
187                     % (var, escapeCString(self.value))]
188         elif self.type == ovs.db.types.UuidType:
189             return ovs.ovsuuid.to_c_assignment(self.value, var)
190
191     def toEnglish(self, escapeLiteral=returnUnchanged):
192         if self.type == ovs.db.types.IntegerType:
193             return '%d' % self.value
194         elif self.type == ovs.db.types.RealType:
195             return '%.15g' % self.value
196         elif self.type == ovs.db.types.BooleanType:
197             if self.value:
198                 return 'true'
199             else:
200                 return 'false'
201         elif self.type == ovs.db.types.StringType:
202             return escapeLiteral(self.value)
203         elif self.type == ovs.db.types.UuidType:
204             return self.value.value
205
206     __need_quotes_re = re.compile("$|true|false|[^_a-zA-Z]|.*[^-._a-zA-Z]")
207     @staticmethod
208     def __string_needs_quotes(s):
209         return Atom.__need_quotes_re.match(s)
210
211     def to_string(self):
212         if self.type == ovs.db.types.IntegerType:
213             return '%d' % self.value
214         elif self.type == ovs.db.types.RealType:
215             return '%.15g' % self.value
216         elif self.type == ovs.db.types.BooleanType:
217             if self.value:
218                 return 'true'
219             else:
220                 return 'false'
221         elif self.type == ovs.db.types.StringType:
222             if Atom.__string_needs_quotes(self.value):
223                 return ovs.json.to_string(self.value)
224             else:
225                 return self.value
226         elif self.type == ovs.db.types.UuidType:
227             return str(self.value)
228
229     @staticmethod
230     def new(x):
231         if type(x) in [int, long]:
232             t = ovs.db.types.IntegerType
233         elif type(x) == float:
234             t = ovs.db.types.RealType
235         elif x in [False, True]:
236             t = ovs.db.types.BooleanType
237         elif type(x) in [str, unicode]:
238             t = ovs.db.types.StringType
239         elif isinstance(x, uuid):
240             t = ovs.db.types.UuidType
241         else:
242             raise TypeError
243         return Atom(t, x)
244
245 class Datum(object):
246     def __init__(self, type_, values={}):
247         self.type = type_
248         self.values = values
249
250     def __cmp__(self, other):
251         if not isinstance(other, Datum):
252             return NotImplemented
253         elif self.values < other.values:
254             return -1
255         elif self.values > other.values:
256             return 1
257         else:
258             return 0
259
260     __hash__ = None
261
262     def __contains__(self, item):
263         return item in self.values
264
265     def copy(self):
266         return Datum(self.type, dict(self.values))
267
268     @staticmethod
269     def default(type_):
270         if type_.n_min == 0:
271             values = {}
272         elif type_.is_map():
273             values = {type_.key.default(): type_.value.default()}
274         else:
275             values = {type_.key.default(): None}
276         return Datum(type_, values)
277
278     def is_default(self):
279         return self == Datum.default(self.type)
280
281     def check_constraints(self):
282         """Checks that each of the atoms in 'datum' conforms to the constraints
283         specified by its 'type' and raises an ovs.db.error.Error.
284
285         This function is not commonly useful because the most ordinary way to
286         obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
287         which check constraints themselves."""
288         for keyAtom, valueAtom in self.values.iteritems():
289             keyAtom.check_constraints(self.type.key)
290             if valueAtom is not None:
291                 valueAtom.check_constraints(self.type.value)
292
293     @staticmethod
294     def from_json(type_, json, symtab=None):
295         """Parses 'json' as a datum of the type described by 'type'.  If
296         successful, returns a new datum.  On failure, raises an
297         ovs.db.error.Error.
298         
299         Violations of constraints expressed by 'type' are treated as errors.
300         
301         If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
302         Refer to ovsdb/SPECS for information about this, and for the syntax
303         that this function accepts."""
304         is_map = type_.is_map()
305         if (is_map or
306             (type(json) == list and len(json) > 0 and json[0] == "set")):
307             if is_map:
308                 class_ = "map"
309             else:
310                 class_ = "set"
311
312             inner = ovs.db.parser.unwrap_json(json, class_, [list, tuple],
313                                               "array")
314             n = len(inner)
315             if n < type_.n_min or n > type_.n_max:
316                 raise error.Error("%s must have %d to %d members but %d are "
317                                   "present" % (class_, type_.n_min,
318                                                type_.n_max, n),
319                                   json)
320
321             values = {}
322             for element in inner:
323                 if is_map:
324                     key, value = ovs.db.parser.parse_json_pair(element)
325                     keyAtom = Atom.from_json(type_.key, key, symtab)
326                     valueAtom = Atom.from_json(type_.value, value, symtab)
327                 else:
328                     keyAtom = Atom.from_json(type_.key, element, symtab)
329                     valueAtom = None
330
331                 if keyAtom in values:
332                     if is_map:
333                         raise error.Error("map contains duplicate key")
334                     else:
335                         raise error.Error("set contains duplicate")
336
337                 values[keyAtom] = valueAtom
338
339             return Datum(type_, values)
340         else:
341             keyAtom = Atom.from_json(type_.key, json, symtab)
342             return Datum(type_, {keyAtom: None})
343
344     def to_json(self):
345         if self.type.is_map():
346             return ["map", [[k.to_json(), v.to_json()]
347                             for k, v in sorted(self.values.items())]]
348         elif len(self.values) == 1:
349             key = self.values.keys()[0]
350             return key.to_json()
351         else:
352             return ["set", [k.to_json() for k in sorted(self.values.keys())]]
353
354     def to_string(self):
355         head = tail = None
356         if self.type.n_max > 1 or len(self.values) == 0:
357             if self.type.is_map():
358                 head = "{"
359                 tail = "}"
360             else:
361                 head = "["
362                 tail = "]"
363
364         s = []
365         if head:
366             s.append(head)
367
368         for i, key in enumerate(sorted(self.values)):
369             if i:
370                 s.append(", ")
371
372             s.append(key.to_string())
373             if self.type.is_map():
374                 s.append("=")
375                 s.append(self.values[key].to_string())
376
377         if tail:
378             s.append(tail)
379         return ''.join(s)
380
381     def as_list(self):
382         if self.type.is_map():
383             return [[k.value, v.value] for k, v in self.values.iteritems()]
384         else:
385             return [k.value for k in self.values.iterkeys()]
386         
387     def as_dict(self):
388         return dict(self.values)
389
390     def as_scalar(self):
391         if len(self.values) == 1:
392             if self.type.is_map():
393                 k, v = self.values.iteritems()[0]
394                 return [k.value, v.value]
395             else:
396                 return self.values.keys()[0].value
397         else:
398             return None
399
400     def to_python(self, uuid_to_row):
401         """Returns this datum's value converted into a natural Python
402         representation of this datum's type, according to the following
403         rules:
404
405         - If the type has exactly one value and it is not a map (that is,
406           self.type.is_scalar() returns True), then the value is:
407
408             * An int or long, for an integer column.
409
410             * An int or long or float, for a real column.
411
412             * A bool, for a boolean column.
413
414             * A str or unicode object, for a string column.
415
416             * A uuid.UUID object, for a UUID column without a ref_table.
417
418             * An object represented the referenced row, for a UUID column with
419               a ref_table.  (For the Idl, this object will be an ovs.db.idl.Row
420               object.)
421
422           If some error occurs (e.g. the database server's idea of the column
423           is different from the IDL's idea), then the default value for the
424           scalar type is used (see Atom.default()).
425
426         - Otherwise, if the type is not a map, then the value is a Python list
427           whose elements have the types described above.
428
429         - Otherwise, the type is a map, and the value is a Python dict that
430           maps from key to value, with key and value types determined as
431           described above.
432
433         'uuid_to_row' must be a function that takes a value and an
434         ovs.db.types.BaseType and translates UUIDs into row objects."""
435         if self.type.is_scalar():
436             value = uuid_to_row(self.as_scalar(), self.type.key)
437             if value is None:
438                 return self.type.key.default()
439             else:
440                 return value
441         elif self.type.is_map():
442             value = {}
443             for k, v in self.values.iteritems():
444                 dk = uuid_to_row(k.value, self.type.key)
445                 dv = uuid_to_row(v.value, self.type.value)
446                 if dk is not None and dv is not None:
447                     value[dk] = dv
448             return value
449         else:
450             s = set()
451             for k in self.values:
452                 dk = uuid_to_row(k.value, self.type.key)
453                 if dk is not None:
454                     s.add(dk)
455             return sorted(s)
456
457     @staticmethod
458     def from_python(type_, value, row_to_uuid):
459         """Returns a new Datum with the given ovs.db.types.Type 'type_'.  The
460         new datum's value is taken from 'value', which must take the form
461         described as a valid return value from Datum.to_python() for 'type'.
462
463         Each scalar value within 'value' is initally passed through
464         'row_to_uuid', which should convert objects that represent rows (if
465         any) into uuid.UUID objects and return other data unchanged.
466
467         Raises ovs.db.error.Error if 'value' is not in an appropriate form for
468         'type_'."""
469         d = {}
470         if type(value) == dict:
471             for k, v in value.iteritems():
472                 ka = Atom.from_python(type_.key, row_to_uuid(k))
473                 va = Atom.from_python(type_.value, row_to_uuid(v))
474                 d[ka] = va
475         elif type(value) in (list, tuple):
476             for k in value:
477                 ka = Atom.from_python(type_.key, row_to_uuid(k))
478                 d[ka] = None
479         else:
480             ka = Atom.from_python(type_.key, row_to_uuid(value))
481             d[ka] = None
482
483         datum = Datum(type_, d)
484         datum.check_constraints()
485         if not datum.conforms_to_type():
486             raise error.Error("%d values when type requires between %d and %d"
487                               % (len(d), type_.n_min, type_.n_max))
488
489         return datum
490
491     def __getitem__(self, key):
492         if not isinstance(key, Atom):
493             key = Atom.new(key)
494         if not self.type.is_map():
495             raise IndexError
496         elif key not in self.values:
497             raise KeyError
498         else:
499             return self.values[key].value
500
501     def get(self, key, default=None):
502         if not isinstance(key, Atom):
503             key = Atom.new(key)
504         if key in self.values:
505             return self.values[key].value
506         else:
507             return default
508         
509     def __str__(self):
510         return self.to_string()
511
512     def conforms_to_type(self):
513         n = len(self.values)
514         return self.type.n_min <= n <= self.type.n_max
515
516     def cInitDatum(self, var):
517         if len(self.values) == 0:
518             return ["ovsdb_datum_init_empty(%s);" % var]
519
520         s = ["%s->n = %d;" % (var, len(self.values))]
521         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
522               % (var, len(self.values), var)]
523
524         for i, key in enumerate(sorted(self.values)):
525             s += key.cInitAtom("%s->keys[%d]" % (var, i))
526         
527         if self.type.value:
528             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
529                   % (var, len(self.values), var)]
530             for i, (key, value) in enumerate(sorted(self.values.items())):
531                 s += value.cInitAtom("%s->values[%d]" % (var, i))
532         else:
533             s += ["%s->values = NULL;" % var]
534
535         if len(self.values) > 1:
536             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
537                   % (var, self.type.key.type.to_string().upper())]
538
539         return s