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