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