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