Implement initial Python bindings for Open vSwitch database.
[sliver-openvswitch.git] / python / ovs / db / data.py
1 # Copyright (c) 2009, 2010 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 errno
16 import logging
17 import os
18 import re
19 import select
20 import sys
21 import uuid
22
23 import ovs.poller
24 import ovs.socket_util
25 import ovs.json
26 import ovs.jsonrpc
27 import ovs.ovsuuid
28
29 import ovs.db.parser
30 from ovs.db import error
31 import ovs.db.types
32
33 class ConstraintViolation(error.Error):
34     def __init__(self, msg, json=None):
35         error.Error.__init__(self, msg, json, tag="constraint violation")
36
37 def escapeCString(src):
38     dst = ""
39     for c in src:
40         if c in "\\\"":
41             dst += "\\" + c
42         elif ord(c) < 32:
43             if c == '\n':
44                 dst += '\\n'
45             elif c == '\r':
46                 dst += '\\r'
47             elif c == '\a':
48                 dst += '\\a'
49             elif c == '\b':
50                 dst += '\\b'
51             elif c == '\f':
52                 dst += '\\f'
53             elif c == '\t':
54                 dst += '\\t'
55             elif c == '\v':
56                 dst += '\\v'
57             else:
58                 dst += '\\%03o' % ord(c)
59         else:
60             dst += c
61     return dst
62
63 def returnUnchanged(x):
64     return x
65
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 __cmp__(self, other):
75         if not isinstance(other, Atom) or self.type != other.type:
76             return NotImplemented
77         elif self.value < other.value:
78             return -1
79         elif self.value > other.value:
80             return 1
81         else:
82             return 0
83
84     def __hash__(self):
85         return hash(self.value)
86
87     @staticmethod
88     def default(type):
89         return Atom(type)
90
91     def is_default(self):
92         return self == default(self.type)
93
94     @staticmethod
95     def from_json(base, json, symtab=None):
96         type_ = base.type
97         json = ovs.db.parser.float_to_int(json)
98         if ((type_ == ovs.db.types.IntegerType and type(json) in [int, long])
99             or (type_ == ovs.db.types.RealType and type(json) in [int, long, float])
100             or (type_ == ovs.db.types.BooleanType and type(json) == bool)
101             or (type_ == ovs.db.types.StringType and type(json) in [str, unicode])):
102             atom = Atom(type_, json)
103         elif type_ == ovs.db.types.UuidType:
104             atom = Atom(type_, ovs.ovsuuid.UUID.from_json(json, symtab))
105         else:
106             raise error.Error("expected %s" % type_.to_string(), json)
107         atom.check_constraints(base)
108         return atom
109
110     def check_constraints(self, base):
111         """Checks whether 'atom' meets the constraints (if any) defined in
112         'base' and raises an ovs.db.error.Error if any constraint is violated.
113
114         'base' and 'atom' must have the same type.
115
116         Checking UUID constraints is deferred to transaction commit time, so
117         this function does nothing for UUID constraints."""
118         assert base.type == self.type
119         if base.enum is not None and self not in base.enum:
120             raise ConstraintViolation(
121                 "%s is not one of the allowed values (%s)"
122                 % (self.to_string(), base.enum.to_string()))
123         elif base.type in [ovs.db.types.IntegerType, ovs.db.types.RealType]:
124             if ((base.min is None or self.value >= base.min) and
125                 (base.max is None or self.value <= base.max)):
126                 pass
127             elif base.min is not None and base.max is not None:
128                 raise ConstraintViolation(
129                     "%s is not in the valid range %.15g to %.15g (inclusive)"
130                     % (self.to_string(), base.min, base.max))
131             elif base.min is not None:
132                 raise ConstraintViolation(
133                     "%s is less than minimum allowed value %.15g"
134                             % (self.to_string(), base.min))
135             else:
136                 raise ConstraintViolation(
137                     "%s is greater than maximum allowed value %.15g"
138                     % (self.to_string(), base.max))
139         elif base.type == ovs.db.types.StringType:
140             # XXX The C version validates that the string is valid UTF-8 here.
141             # Do we need to do that in Python too?
142             s = self.value
143             length = len(s)
144             if length < base.min_length:
145                 raise ConstraintViolation(
146                     "\"%s\" length %d is less than minimum allowed length %d"
147                     % (s, length, base.min_length))
148             elif length > base.max_length:
149                 raise ConstraintViolation(
150                     "\"%s\" length %d is greater than maximum allowed "
151                     "length %d" % (s, length, base.max_length))
152     
153     def to_json(self):
154         if self.type == ovs.db.types.UuidType:
155             return self.value.to_json()
156         else:
157             return self.value
158
159     def cInitAtom(self, var):
160         if self.type == ovs.db.types.IntegerType:
161             return ['%s.integer = %d;' % (var, self.value)]
162         elif self.type == ovs.db.types.RealType:
163             return ['%s.real = %.15g;' % (var, self.value)]
164         elif self.type == ovs.db.types.BooleanType:
165             if self.value:
166                 return ['%s.boolean = true;']
167             else:
168                 return ['%s.boolean = false;']
169         elif self.type == ovs.db.types.StringType:
170             return ['%s.string = xstrdup("%s");'
171                     % (var, escapeCString(self.value))]
172         elif self.type == ovs.db.types.UuidType:
173             return self.value.cInitUUID(var)
174
175     def toEnglish(self, escapeLiteral=returnUnchanged):
176         if self.type == ovs.db.types.IntegerType:
177             return '%d' % self.value
178         elif self.type == ovs.db.types.RealType:
179             return '%.15g' % self.value
180         elif self.type == ovs.db.types.BooleanType:
181             if self.value:
182                 return 'true'
183             else:
184                 return 'false'
185         elif self.type == ovs.db.types.StringType:
186             return escapeLiteral(self.value)
187         elif self.type == ovs.db.types.UuidType:
188             return self.value.value
189
190     __need_quotes_re = re.compile("$|true|false|[^_a-zA-Z]|.*[^-._a-zA-Z]")
191     @staticmethod
192     def __string_needs_quotes(s):
193         return Atom.__need_quotes_re.match(s)
194
195     def to_string(self):
196         if self.type == ovs.db.types.IntegerType:
197             return '%d' % self.value
198         elif self.type == ovs.db.types.RealType:
199             return '%.15g' % self.value
200         elif self.type == ovs.db.types.BooleanType:
201             if self.value:
202                 return 'true'
203             else:
204                 return 'false'
205         elif self.type == ovs.db.types.StringType:
206             if Atom.__string_needs_quotes(self.value):
207                 return ovs.json.to_string(self.value)
208             else:
209                 return self.value
210         elif self.type == ovs.db.types.UuidType:
211             return str(self.value)
212
213     @staticmethod
214     def new(x):
215         if type(x) in [int, long]:
216             t = ovs.db.types.IntegerType
217         elif type(x) == float:
218             t = ovs.db.types.RealType
219         elif x in [False, True]:
220             t = ovs.db.types.RealType
221         elif type(x) in [str, unicode]:
222             t = ovs.db.types.StringType
223         elif isinstance(x, uuid):
224             t = ovs.db.types.UuidType
225         else:
226             raise TypeError
227         return Atom(t, x)
228
229 class Datum(object):
230     def __init__(self, type, values={}):
231         self.type = type
232         self.values = values
233
234     def __cmp__(self, other):
235         if not isinstance(other, Datum):
236             return NotImplemented
237         elif self.values < other.values:
238             return -1
239         elif self.values > other.values:
240             return 1
241         else:
242             return 0
243
244     __hash__ = None
245
246     def __contains__(self, item):
247         return item in self.values
248
249     def clone(self):
250         return Datum(self.type, dict(self.values))
251
252     @staticmethod
253     def default(type):
254         if type.n_min == 0:
255             values = {}
256         elif type.is_map():
257             values = {type.key.default(): type.value.default()}
258         else:
259             values = {type.key.default(): None}
260         return Datum(type, values)
261
262     @staticmethod
263     def is_default(self):
264         return self == default(self.type)
265
266     def check_constraints(self):
267         """Checks that each of the atoms in 'datum' conforms to the constraints
268         specified by its 'type' and raises an ovs.db.error.Error.
269
270         This function is not commonly useful because the most ordinary way to
271         obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
272         which check constraints themselves."""
273         for keyAtom, valueAtom in self.values:
274             keyAtom.check_constraints()
275             if valueAtom is not None:
276                 valueAtom.check_constraints()
277
278     @staticmethod
279     def from_json(type_, json, symtab=None):
280         """Parses 'json' as a datum of the type described by 'type'.  If
281         successful, returns a new datum.  On failure, raises an
282         ovs.db.error.Error.
283         
284         Violations of constraints expressed by 'type' are treated as errors.
285         
286         If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
287         Refer to ovsdb/SPECS for information about this, and for the syntax
288         that this function accepts."""
289         is_map = type_.is_map()
290         if (is_map or
291             (type(json) == list and len(json) > 0 and json[0] == "set")):
292             if is_map:
293                 class_ = "map"
294             else:
295                 class_ = "set"
296
297             inner = ovs.db.parser.unwrap_json(json, class_, list)
298             n = len(inner)
299             if n < type_.n_min or n > type_.n_max:
300                 raise error.Error("%s must have %d to %d members but %d are "
301                                   "present" % (class_, type_.n_min,
302                                                type_.n_max, n),
303                                   json)
304
305             values = {}
306             for element in inner:
307                 if is_map:
308                     key, value = ovs.db.parser.parse_json_pair(element)
309                     keyAtom = Atom.from_json(type_.key, key, symtab)
310                     valueAtom = Atom.from_json(type_.value, value, symtab)
311                 else:
312                     keyAtom = Atom.from_json(type_.key, element, symtab)
313                     valueAtom = None
314
315                 if keyAtom in values:
316                     if is_map:
317                         raise error.Error("map contains duplicate key")
318                     else:
319                         raise error.Error("set contains duplicate")
320
321                 values[keyAtom] = valueAtom
322
323             return Datum(type_, values)
324         else:
325             keyAtom = Atom.from_json(type_.key, json, symtab)
326             return Datum(type_, {keyAtom: None})
327
328     def to_json(self):
329         if len(self.values) == 1 and not self.type.is_map():
330             key = self.values.keys()[0]
331             return key.to_json()
332         elif not self.type.is_map():
333             return ["set", [k.to_json() for k in sorted(self.values.keys())]]
334         else:
335             return ["map", [[k.to_json(), v.to_json()]
336                             for k, v in sorted(self.values.items())]]
337
338     def to_string(self):
339         if self.type.n_max > 1 or len(self.values) == 0:
340             if self.type.is_map():
341                 s = "{"
342             else:
343                 s = "["
344         else:
345             s = ""
346
347         i = 0
348         for key in sorted(self.values):
349             if i > 0:
350                 s += ", "
351             i += 1
352
353             if self.type.is_map():
354                 s += "%s=%s" % (key.to_string(), self.values[key].to_string())
355             else:
356                 s += key.to_string()
357
358         if self.type.n_max > 1 or len(self.values) == 0:
359             if self.type.is_map():
360                 s += "}"
361             else:
362                 s += "]"
363         return s
364
365     def as_list(self):
366         if self.type.is_map():
367             return [[k.value, v.value] for k, v in self.values.iteritems()]
368         else:
369             return [k.value for k in self.values.iterkeys()]
370         
371     def as_scalar(self):
372         if len(self.values) == 1:
373             if self.type.is_map():
374                 k, v = self.values.iteritems()[0]
375                 return [k.value, v.value]
376             else:
377                 return self.values.keys()[0].value
378         else:
379             return None
380
381     def __getitem__(self, key):
382         if not isinstance(key, Atom):
383             key = Atom.new(key)
384         if not self.type.is_map():
385             raise IndexError
386         elif key not in self.values:
387             raise KeyError
388         else:
389             return self.values[key].value
390
391     def get(self, key, default=None):
392         if not isinstance(key, Atom):
393             key = Atom.new(key)
394         if key in self.values:
395             return self.values[key].value
396         else:
397             return default
398         
399     def __str__(self):
400         return self.to_string()
401
402     def conforms_to_type(self):
403         n = len(self.values)
404         return n >= self.type.n_min and n <= self.type.n_max
405
406     def cInitDatum(self, var):
407         if len(self.values) == 0:
408             return ["ovsdb_datum_init_empty(%s);" % var]
409
410         s = ["%s->n = %d;" % (var, len(self.values))]
411         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
412               % (var, len(self.values), var)]
413
414         i = 0
415         for key, value in sorted(self.values.items()):
416             s += key.cInitAtom("%s->keys[%d]" % (var, i))
417             i += 1
418         
419         if self.type.value:
420             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
421                   % (var, len(self.values), var)]
422             i = 0
423             for key, value in sorted(self.values.items()):
424                 s += value.cInitAtom("%s->values[%d]" % (var, i))
425                 i += 1
426         else:
427             s += ["%s->values = NULL;" % var]
428
429         if len(self.values) > 1:
430             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
431                   % (var, self.type.key.type.to_string().upper())]
432
433         return s