Add documentation for the vswitch database schema.
[sliver-openvswitch.git] / ovsdb / OVSDB.py
1 import re
2
3 class Error(Exception):
4     def __init__(self, msg):
5         Exception.__init__(self)
6         self.msg = msg
7
8 def getMember(json, name, validTypes, description, default=None):
9     if name in json:
10         member = json[name]
11         if len(validTypes) and type(member) not in validTypes:
12             raise Error("%s: type mismatch for '%s' member"
13                         % (description, name))
14         return member
15     return default
16
17 def mustGetMember(json, name, expectedType, description):
18     member = getMember(json, name, expectedType, description)
19     if member == None:
20         raise Error("%s: missing '%s' member" % (description, name))
21     return member
22
23 class DbSchema:
24     def __init__(self, name, comment, tables):
25         self.name = name
26         self.comment = comment
27         self.tables = tables
28
29     @staticmethod
30     def fromJson(json):
31         name = mustGetMember(json, 'name', [unicode], 'database')
32         comment = getMember(json, 'comment', [unicode], 'database')
33         tablesJson = mustGetMember(json, 'tables', [dict], 'database')
34         tables = {}
35         for tableName, tableJson in tablesJson.iteritems():
36             tables[tableName] = TableSchema.fromJson(tableJson,
37                                                      "%s table" % tableName)
38         return DbSchema(name, comment, tables)
39
40 class IdlSchema(DbSchema):
41     def __init__(self, name, comment, tables, idlPrefix, idlHeader):
42         DbSchema.__init__(self, name, comment, tables)
43         self.idlPrefix = idlPrefix
44         self.idlHeader = idlHeader
45
46     @staticmethod
47     def fromJson(json):
48         schema = DbSchema.fromJson(json)
49         idlPrefix = mustGetMember(json, 'idlPrefix', [unicode], 'database')
50         idlHeader = mustGetMember(json, 'idlHeader', [unicode], 'database')
51         return IdlSchema(schema.name, schema.comment, schema.tables,
52                          idlPrefix, idlHeader)
53
54 class TableSchema:
55     def __init__(self, comment, columns):
56         self.comment = comment
57         self.columns = columns
58
59     @staticmethod
60     def fromJson(json, description):
61         comment = getMember(json, 'comment', [unicode], description)
62         columnsJson = mustGetMember(json, 'columns', [dict], description)
63         columns = {}
64         for name, json in columnsJson.iteritems():
65             columns[name] = ColumnSchema.fromJson(
66                 json, "column %s in %s" % (name, description))
67         return TableSchema(comment, columns)
68
69 class ColumnSchema:
70     def __init__(self, comment, type, persistent):
71         self.comment = comment
72         self.type = type
73         self.persistent = persistent
74
75     @staticmethod
76     def fromJson(json, description):
77         comment = getMember(json, 'comment', [unicode], description)
78         type = Type.fromJson(mustGetMember(json, 'type', [dict, unicode],
79                                            description),
80                              'type of %s' % description)
81         ephemeral = getMember(json, 'ephemeral', [bool], description)
82         persistent = ephemeral != True
83         return ColumnSchema(comment, type, persistent)
84
85 def escapeCString(src):
86     dst = ""
87     for c in src:
88         if c in "\\\"":
89             dst += "\\" + c
90         elif ord(c) < 32:
91             if c == '\n':
92                 dst += '\\n'
93             elif c == '\r':
94                 dst += '\\r'
95             elif c == '\a':
96                 dst += '\\a'
97             elif c == '\b':
98                 dst += '\\b'
99             elif c == '\f':
100                 dst += '\\f'
101             elif c == '\t':
102                 dst += '\\t'
103             elif c == '\v':
104                 dst += '\\v'
105             else:
106                 dst += '\\%03o' % ord(c)
107         else:
108             dst += c
109     return dst
110
111 def returnUnchanged(x):
112     return x
113
114 class UUID:
115     x = "[0-9a-fA-f]"
116     uuidRE = re.compile("^(%s{8})-(%s{4})-(%s{4})-(%s{4})-(%s{4})(%s{8})$"
117                         % (x, x, x, x, x, x))
118
119     def __init__(self, value):
120         self.value = value
121
122     @staticmethod
123     def fromString(s):
124         if not uuidRE.match(s):
125             raise Error("%s is not a valid UUID" % s)
126         return UUID(s)
127
128     @staticmethod
129     def fromJson(json):
130         if UUID.isValidJson(json):
131             return UUID(json[1])
132         else:
133             raise Error("%s is not valid JSON for a UUID" % json)
134
135     @staticmethod
136     def isValidJson(json):
137         return len(json) == 2 and json[0] == "uuid" and uuidRE.match(json[1])
138             
139     def toJson(self):
140         return ["uuid", self.value]
141
142     def cInitUUID(self, var):
143         m = re.match(self.value)
144         return ["%s.parts[0] = 0x%s;" % (var, m.group(1)),
145                 "%s.parts[1] = 0x%s%s;" % (var, m.group(2), m.group(3)),
146                 "%s.parts[2] = 0x%s%s;" % (var, m.group(4), m.group(5)),
147                 "%s.parts[3] = 0x%s;" % (var, m.group(6))]
148
149 class Atom:
150     def __init__(self, type, value):
151         self.type = type
152         self.value = value
153
154     @staticmethod
155     def fromJson(type_, json):
156         if ((type_ == 'integer' and type(json) in [int, long])
157             or (type_ == 'real' and type(json) in [int, long, float])
158             or (type_ == 'boolean' and json in [True, False])
159             or (type_ == 'string' and type(json) in [str, unicode])):
160             return Atom(type_, json)
161         elif type_ == 'uuid':
162             return UUID.fromJson(json)
163         else:
164             raise Error("%s is not valid JSON for type %s" % (json, type_))
165
166     def toJson(self):
167         if self.type == 'uuid':
168             return self.value.toString()
169         else:
170             return self.value
171
172     def cInitAtom(self, var):
173         if self.type == 'integer':
174             return ['%s.integer = %d;' % (var, self.value)]
175         elif self.type == 'real':
176             return ['%s.real = %.15g;' % (var, self.value)]
177         elif self.type == 'boolean':
178             if self.value:
179                 return ['%s.boolean = true;']
180             else:
181                 return ['%s.boolean = false;']
182         elif self.type == 'string':
183             return ['%s.string = xstrdup("%s");'
184                     % (var, escapeCString(self.value))]
185         elif self.type == 'uuid':
186             return self.value.cInitUUID(var)
187
188     def toEnglish(self, escapeLiteral=returnUnchanged):
189         if self.type == 'integer':
190             return '%d' % self.value
191         elif self.type == 'real':
192             return '%.15g' % self.value
193         elif self.type == 'boolean':
194             if self.value:
195                 return 'true'
196             else:
197                 return 'false'
198         elif self.type == 'string':
199             return escapeLiteral(self.value)
200         elif self.type == 'uuid':
201             return self.value.value
202
203 class BaseType:
204     def __init__(self, type,
205                  enum=None,
206                  refTable=None,
207                  minInteger=None, maxInteger=None,
208                  minReal=None, maxReal=None,
209                  minLength=None, maxLength=None):
210         self.type = type
211         self.enum = enum
212         self.refTable = refTable
213         self.minInteger = minInteger
214         self.maxInteger = maxInteger
215         self.minReal = minReal
216         self.maxReal = maxReal
217         self.minLength = minLength
218         self.maxLength = maxLength
219
220     @staticmethod
221     def fromJson(json, description):
222         if type(json) == unicode:
223             return BaseType(json)
224         else:
225             atomicType = mustGetMember(json, 'type', [unicode], description)
226             enum = getMember(json, 'enum', [], description)
227             if enum:
228                 enumType = Type(atomicType, None, 0, 'unlimited')
229                 enum = Datum.fromJson(enumType, enum)
230             refTable = getMember(json, 'refTable', [unicode], description)
231             minInteger = getMember(json, 'minInteger', [int, long], description)
232             maxInteger = getMember(json, 'maxInteger', [int, long], description)
233             minReal = getMember(json, 'minReal', [int, long, float], description)
234             maxReal = getMember(json, 'maxReal', [int, long, float], description)
235             minLength = getMember(json, 'minLength', [int], description)
236             maxLength = getMember(json, 'minLength', [int], description)
237             return BaseType(atomicType, enum, refTable, minInteger, maxInteger, minReal, maxReal, minLength, maxLength)
238
239     def toEnglish(self, escapeLiteral=returnUnchanged):
240         if self.type == 'uuid' and self.refTable:
241             return escapeLiteral(self.refTable)
242         else:
243             return self.type
244
245     def constraintsToEnglish(self, escapeLiteral=returnUnchanged):
246         if self.enum:
247             literals = [value.toEnglish(escapeLiteral)
248                         for value in self.enum.values]
249             if len(literals) == 2:
250                 return 'either %s or %s' % (literals[0], literals[1])
251             else:
252                 return 'one of %s, %s, or %s' % (literals[0],
253                                                  ', '.join(literals[1:-1]),
254                                                  literals[-1])
255         elif self.minInteger != None and self.maxInteger != None:
256             return 'in range [%d,%d]' % (self.minInteger, self.maxInteger)
257         elif self.minInteger != None:
258             return 'at least %d' % self.minInteger
259         elif self.maxInteger != None:
260             return 'at most %d' % self.maxInteger
261         elif self.minReal != None and self.maxReal != None:
262             return 'in range [%g, %g]' % (self.minReal, self.maxReal)
263         elif self.minReal != None:
264             return 'at least %g' % self.minReal
265         elif self.maxReal != None:
266             return 'at most %g' % self.maxReal
267         elif self.minLength != None and self.maxLength != None:
268             if self.minLength == self.maxLength:
269                 return 'exactly %d characters long' % (self.minLength)
270             else:
271                 return 'between %d and %d characters long' % (self.minLength, self.maxLength)
272         elif self.minLength != None:
273             return 'at least %d characters long' % self.minLength
274         elif self.maxLength != None:
275             return 'at most %d characters long' % self.maxLength
276         else:
277             return ''
278
279     def toCType(self, prefix):
280         if self.refTable:
281             return "struct %s%s *" % (prefix, self.refTable.lower())
282         else:
283             return {'integer': 'int64_t ',
284                     'real': 'double ',
285                     'uuid': 'struct uuid ',
286                     'boolean': 'bool ',
287                     'string': 'char *'}[self.type]
288
289     def copyCValue(self, dst, src):
290         args = {'dst': dst, 'src': src}
291         if self.refTable:
292             return ("%(dst)s = %(src)s->header_.uuid;") % args
293         elif self.type == 'string':
294             return "%(dst)s = xstrdup(%(src)s);" % args
295         else:
296             return "%(dst)s = %(src)s;" % args
297
298     def initCDefault(self, var, isOptional):
299         if self.refTable:
300             return "%s = NULL;" % var
301         elif self.type == 'string' and not isOptional:
302             return "%s = \"\";" % var
303         else:
304             return {'integer': '%s = 0;',
305                     'real': '%s = 0.0;',
306                     'uuid': 'uuid_zero(&%s);',
307                     'boolean': '%s = false;',
308                     'string': '%s = NULL;'}[self.type] % var
309
310     def cInitBaseType(self, indent, var):
311         stmts = []
312         stmts.append('ovsdb_base_type_init(&%s, OVSDB_TYPE_%s);' % (
313                 var, self.type.upper()),)
314         if self.enum:
315             stmts.append("%s.enum_ = xmalloc(sizeof *%s.enum_);"
316                          % (var, var))
317             stmts += self.enum.cInitDatum("%s.enum_" % var)
318         if self.type == 'integer':
319             if self.minInteger != None:
320                 stmts.append('%s.u.integer.min = %d;' % (var, self.minInteger))
321             if self.maxInteger != None:
322                 stmts.append('%s.u.integer.max = %d;' % (var, self.maxInteger))
323         elif self.type == 'real':
324             if self.minReal != None:
325                 stmts.append('%s.u.real.min = %d;' % (var, self.minReal))
326             if self.maxReal != None:
327                 stmts.append('%s.u.real.max = %d;' % (var, self.maxReal))
328         elif self.type == 'string':
329             if self.minLength != None:
330                 stmts.append('%s.u.string.minLen = %d;' % (var, self.minLength))            
331             if self.maxLength != None:
332                 stmts.append('%s.u.string.maxLen = %d;' % (var, self.maxLength))
333         elif self.type == 'uuid':
334             if self.refTable != None:
335                 stmts.append('%s.u.uuid.refTableName = "%s";' % (var, escapeCString(self.refTable)))
336         return '\n'.join([indent + stmt for stmt in stmts])
337
338 class Type:
339     def __init__(self, key, value=None, min=1, max=1):
340         self.key = key
341         self.value = value
342         self.min = min
343         self.max = max
344     
345     @staticmethod
346     def fromJson(json, description):
347         if type(json) == unicode:
348             return Type(BaseType(json))
349         else:
350             keyJson = mustGetMember(json, 'key', [dict, unicode], description)
351             key = BaseType.fromJson(keyJson, 'key in %s' % description)
352
353             valueJson = getMember(json, 'value', [dict, unicode], description)
354             if valueJson:
355                 value = BaseType.fromJson(valueJson,
356                                           'value in %s' % description)
357             else:
358                 value = None
359
360             min = getMember(json, 'min', [int], description, 1)
361             max = getMember(json, 'max', [int, unicode], description, 1)
362             return Type(key, value, min, max)
363
364     def isScalar(self):
365         return self.min == 1 and self.max == 1 and not self.value
366
367     def isOptional(self):
368         return self.min == 0 and self.max == 1
369
370     def isOptionalPointer(self):
371         return (self.min == 0 and self.max == 1 and not self.value
372                 and (self.key.type == 'string' or self.key.refTable))
373
374     def toEnglish(self, escapeLiteral=returnUnchanged):
375         keyName = self.key.toEnglish(escapeLiteral)
376         if self.value:
377             valueName = self.value.toEnglish(escapeLiteral)
378
379         if self.isScalar():
380             return keyName
381         elif self.isOptional():
382             if self.value:
383                 return "optional %s-%s pair" % (keyName, valueName)
384             else:
385                 return "optional %s" % keyName
386         else:
387             if self.max == "unlimited":
388                 if self.min:
389                     quantity = "%d or more " % self.min
390                 else:
391                     quantity = ""
392             elif self.min:
393                 quantity = "%d to %d " % (self.min, self.max)
394             else:
395                 quantity = "up to %d " % self.max
396
397             if self.value:
398                 return "map of %s%s-%s pairs" % (quantity, keyName, valueName)
399             else:
400                 if keyName.endswith('s'):
401                     plural = keyName + "es"
402                 else:
403                     plural = keyName + "s"
404                 return "set of %s%s" % (quantity, plural)
405
406     def constraintsToEnglish(self, escapeLiteral=returnUnchanged):
407         s = ""
408
409         constraints = []
410         keyConstraints = self.key.constraintsToEnglish(escapeLiteral)
411         if keyConstraints:
412             if self.value:
413                 constraints += ['key ' + keyConstraints]
414             else:
415                 constraints += [keyConstraints]
416
417         if self.value:
418             valueConstraints = self.value.constraintsToEnglish(escapeLiteral)
419             if valueConstraints:
420                 constraints += ['value ' + valueConstraints]
421
422         return ', '.join(constraints)
423                 
424     def cDeclComment(self):
425         if self.min == 1 and self.max == 1 and self.key.type == "string":
426             return "\t/* Always nonnull. */"
427         else:
428             return ""
429
430     def cInitType(self, indent, var):
431         initKey = self.key.cInitBaseType(indent, "%s.key" % var)
432         if self.value:
433             initValue = self.value.cInitBaseType(indent, "%s.value" % var)
434         else:
435             initValue = ('%sovsdb_base_type_init(&%s.value, '
436                          'OVSDB_TYPE_VOID);' % (indent, var))
437         initMin = "%s%s.n_min = %s;" % (indent, var, self.min)
438         if self.max == "unlimited":
439             max = "UINT_MAX"
440         else:
441             max = self.max
442         initMax = "%s%s.n_max = %s;" % (indent, var, max)
443         return "\n".join((initKey, initValue, initMin, initMax))
444
445 class Datum:
446     def __init__(self, type, values):
447         self.type = type
448         self.values = values
449
450     @staticmethod
451     def fromJson(type_, json):
452         if not type_.value:
453             if len(json) == 2 and json[0] == "set":
454                 values = []
455                 for atomJson in json[1]:
456                     values += [Atom.fromJson(type_.key, atomJson)]
457             else:
458                 values = [Atom.fromJson(type_.key, json)]
459         else:
460             if len(json) != 2 or json[0] != "map":
461                 raise Error("%s is not valid JSON for a map" % json)
462             values = []
463             for pairJson in json[1]:
464                 values += [(Atom.fromJson(type_.key, pairJson[0]),
465                             Atom.fromJson(type_.value, pairJson[1]))]
466         return Datum(type_, values)
467
468     def cInitDatum(self, var):
469         if len(self.values) == 0:
470             return ["ovsdb_datum_init_empty(%s);" % var]
471
472         s = ["%s->n = %d;" % (var, len(self.values))]
473         s += ["%s->keys = xmalloc(%d * sizeof *%s->keys);"
474               % (var, len(self.values), var)]
475
476         for i in range(len(self.values)):
477             key = self.values[i]
478             if self.type.value:
479                 key = key[0]
480             s += key.cInitAtom("%s->keys[%d]" % (var, i))
481         
482         if self.type.value:
483             s += ["%s->values = xmalloc(%d * sizeof *%s->values);"
484                   % (var, len(self.values), var)]
485             for i in range(len(self.values)):
486                 value = self.values[i][1]
487                 s += key.cInitAtom("%s->values[%d]" % (var, i))
488         else:
489             s += ["%s->values = NULL;" % var]
490
491         if len(self.values) > 1:
492             s += ["ovsdb_datum_sort_assert(%s, OVSDB_TYPE_%s);"
493                   % (var, self.type.key.upper())]
494
495         return s