python: Take advantage of Python "x < y < z" syntax.
[sliver-openvswitch.git] / python / ovs / db / data.py
1 # Copyright (c) 2009, 2010, 2011 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.append("\\" + c)
42         elif ord(c) < 32:
43             if c == '\n':
44                 dst.append('\\n')
45             elif c == '\r':
46                 dst.append('\\r')
47             elif c == '\a':
48                 dst.append('\\a')
49             elif c == '\b':
50                 dst.append('\\b')
51             elif c == '\f':
52                 dst.append('\\f')
53             elif c == '\t':
54                 dst.append('\\t')
55             elif c == '\v':
56                 dst.append('\\v')
57             else:
58                 dst.append('\\%03o' % ord(c))
59         else:
60             dst.append(c)
61     return ''.join(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 == 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     def is_default(self):
263         return self == Datum.default(self.type)
264
265     def check_constraints(self):
266         """Checks that each of the atoms in 'datum' conforms to the constraints
267         specified by its 'type' and raises an ovs.db.error.Error.
268
269         This function is not commonly useful because the most ordinary way to
270         obtain a datum is ultimately via Datum.from_json() or Atom.from_json(),
271         which check constraints themselves."""
272         for keyAtom, valueAtom in self.values:
273             keyAtom.check_constraints()
274             if valueAtom is not None:
275                 valueAtom.check_constraints()
276
277     @staticmethod
278     def from_json(type_, json, symtab=None):
279         """Parses 'json' as a datum of the type described by 'type'.  If
280         successful, returns a new datum.  On failure, raises an
281         ovs.db.error.Error.
282         
283         Violations of constraints expressed by 'type' are treated as errors.
284         
285         If 'symtab' is nonnull, then named UUIDs in 'symtab' are accepted.
286         Refer to ovsdb/SPECS for information about this, and for the syntax
287         that this function accepts."""
288         is_map = type_.is_map()
289         if (is_map or
290             (type(json) == list and len(json) > 0 and json[0] == "set")):
291             if is_map:
292                 class_ = "map"
293             else:
294                 class_ = "set"
295
296             inner = ovs.db.parser.unwrap_json(json, class_, list)
297             n = len(inner)
298             if n < type_.n_min or n > type_.n_max:
299                 raise error.Error("%s must have %d to %d members but %d are "
300                                   "present" % (class_, type_.n_min,
301                                                type_.n_max, n),
302                                   json)
303
304             values = {}
305             for element in inner:
306                 if is_map:
307                     key, value = ovs.db.parser.parse_json_pair(element)
308                     keyAtom = Atom.from_json(type_.key, key, symtab)
309                     valueAtom = Atom.from_json(type_.value, value, symtab)
310                 else:
311                     keyAtom = Atom.from_json(type_.key, element, symtab)
312                     valueAtom = None
313
314                 if keyAtom in values:
315                     if is_map:
316                         raise error.Error("map contains duplicate key")
317                     else:
318                         raise error.Error("set contains duplicate")
319
320                 values[keyAtom] = valueAtom
321
322             return Datum(type_, values)
323         else:
324             keyAtom = Atom.from_json(type_.key, json, symtab)
325             return Datum(type_, {keyAtom: None})
326
327     def to_json(self):
328         if len(self.values) == 1 and not self.type.is_map():
329             key = self.values.keys()[0]
330             return key.to_json()
331         elif not self.type.is_map():
332             return ["set", [k.to_json() for k in sorted(self.values.keys())]]
333         else:
334             return ["map", [[k.to_json(), v.to_json()]
335                             for k, v in sorted(self.values.items())]]
336
337     def to_string(self):
338         head = tail = None
339         if self.type.n_max > 1 or len(self.values) == 0:
340             if self.type.is_map():
341                 head = "{"
342                 tail = "}"
343             else:
344                 head = "["
345                 tail = "]"
346
347         s = []
348         if head:
349             s.append(head)
350
351         i = 0
352         for key in sorted(self.values):
353             if i:
354                 s.append(", ")
355             i += 1
356
357             s.append(key.to_string())
358             if self.type.is_map():
359                 s.append("=")
360                 s.append(self.values[key].to_string())
361
362         if tail:
363             s.append(tail)
364         return ''.join(s)
365
366     def as_list(self):
367         if self.type.is_map():
368             return [[k.value, v.value] for k, v in self.values.iteritems()]
369         else:
370             return [k.value for k in self.values.iterkeys()]
371         
372     def as_scalar(self):
373         if len(self.values) == 1:
374             if self.type.is_map():
375                 k, v = self.values.iteritems()[0]
376                 return [k.value, v.value]
377             else:
378                 return self.values.keys()[0].value
379         else:
380             return None
381
382     def __getitem__(self, key):
383         if not isinstance(key, Atom):
384             key = Atom.new(key)
385         if not self.type.is_map():
386             raise IndexError
387         elif key not in self.values:
388             raise KeyError
389         else:
390             return self.values[key].value
391
392     def get(self, key, default=None):
393         if not isinstance(key, Atom):
394             key = Atom.new(key)
395         if key in self.values:
396             return self.values[key].value
397         else:
398             return default
399         
400     def __str__(self):
401         return self.to_string()
402
403     def conforms_to_type(self):
404         n = len(self.values)
405         return self.type.n_min <= n <= self.type.n_max
406
407     def cInitDatum(self, var):
408         if len(self.values) == 0:
409             return ["ovsdb_datum_init_empty(%s);" % var]
410
411         s = ["%s->n = %d;" % (var, len(self.values))]
412         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
413               % (var, len(self.values), var)]
414
415         i = 0
416         for key, value in sorted(self.values.items()):
417             s += key.cInitAtom("%s->keys[%d]" % (var, i))
418             i += 1
419         
420         if self.type.value:
421             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
422                   % (var, len(self.values), var)]
423             i = 0
424             for key, value in sorted(self.values.items()):
425                 s += value.cInitAtom("%s->values[%d]" % (var, i))
426                 i += 1
427         else:
428             s += ["%s->values = NULL;" % var]
429
430         if len(self.values) > 1:
431             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
432                   % (var, self.type.key.type.to_string().upper())]
433
434         return s