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