blob: 392dd846420e7388ebad5c703ff753c4846d7fb1 [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001
2# Copyright 2017-present Open Networking Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16
Scott Baker96b995a2017-02-15 16:21:12 -080017"""
18Django-like ORM layer for gRPC
19
20Usage:
21 api = ORMStub(stub)
22
23 api.Slices.all() ... list all slices
24
25 someSlice = api.Slices.get(id=1) ... get slice #1
26
27 someSlice.site ... automatically resolves site_id into a site object
28 someSlice.instances ... automatically resolves instances_ids into instance objects
29 someSlice.save() ... saves the slice object
30"""
31
32"""
33import grpc_client, orm
34c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
Scott Bakera1eae7a2017-06-06 09:20:15 -070035u=c.xos_orm.User.objects.get(id=1)
36"""
Scott Baker96b995a2017-02-15 16:21:12 -080037
Scott Bakerb06e3e02017-12-12 11:05:53 -080038import os
39import sys
Scott Bakerd8246712018-07-12 18:08:31 -070040import threading
Scott Bakeref8d85d2017-02-21 16:44:28 -080041import time
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +020042import imp
Matteo Scandoloe3d2f262018-06-05 17:45:39 -070043from xosconfig import Config
44from multistructlog import create_logger
45
46log = create_logger(Config().get('logging'))
Scott Baker96b995a2017-02-15 16:21:12 -080047
Scott Baker22796cc2017-02-23 16:53:34 -080048convenience_wrappers = {}
49
Scott Bakerd0f1dc12018-04-23 12:05:32 -070050class ORMGenericContentNotFoundException(Exception):
51 pass
52
53class ORMGenericObjectNotFoundException(Exception):
54 pass
55
Scott Baker96b995a2017-02-15 16:21:12 -080056class ORMWrapper(object):
57 """ Wraps a protobuf object to provide ORM features """
58
59 def __init__(self, wrapped_class, stub, is_new=False):
60 super(ORMWrapper, self).__setattr__("_wrapped_class", wrapped_class)
61 super(ORMWrapper, self).__setattr__("stub", stub)
62 super(ORMWrapper, self).__setattr__("cache", {})
63 super(ORMWrapper, self).__setattr__("reverse_cache", {})
Sapan Bhatia71f57682017-08-23 20:09:08 -040064 super(ORMWrapper, self).__setattr__("synchronizer_step", None)
Sapan Bhatia2b307f72017-11-02 11:39:17 -040065 super(ORMWrapper, self).__setattr__("dependent", None)
Scott Baker96b995a2017-02-15 16:21:12 -080066 super(ORMWrapper, self).__setattr__("is_new", is_new)
Scott Bakerc4156c32017-12-08 10:58:21 -080067 super(ORMWrapper, self).__setattr__("post_save_fixups", [])
Scott Baker96b995a2017-02-15 16:21:12 -080068 fkmap=self.gen_fkmap()
69 super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
70 reverse_fkmap=self.gen_reverse_fkmap()
71 super(ORMWrapper, self).__setattr__("_reverse_fkmap", reverse_fkmap)
72
Scott Bakerd78f6472017-03-14 17:30:14 -070073 def create_attr(self, name, value=None):
74 """ setattr(self, ...) will fail for attributes that don't exist in the
75 wrapped grpc class. This is by design. However, if someone really
76 does want to attach a new attribute to this class, then they can
77 call create_attr()
78 """
79 super(ORMWrapper, self).__setattr__(name, value)
80
Scott Baker03a163f2017-05-17 09:21:47 -070081 def get_generic_foreignkeys(self):
82 """ this is a placeholder until generic foreign key support is added
83 to xproto.
84 """
85 return []
86
Scott Baker96b995a2017-02-15 16:21:12 -080087 def gen_fkmap(self):
88 fkmap = {}
89
Scott Bakeraa556b02017-03-07 16:07:34 -080090 all_field_names = self._wrapped_class.DESCRIPTOR.fields_by_name.keys()
91
Scott Baker96b995a2017-02-15 16:21:12 -080092 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
93 if name.endswith("_id"):
94 foreignKey = field.GetOptions().Extensions._FindExtensionByName("xos.foreignKey")
95 fk = field.GetOptions().Extensions[foreignKey]
Scott Bakeraa556b02017-03-07 16:07:34 -080096 if fk and fk.modelName:
Scott Bakerc4156c32017-12-08 10:58:21 -080097 fkdict = {"src_fieldName": name, "modelName": fk.modelName, "kind": "fk"}
98 if fk.reverseFieldName:
99 fkdict["reverse_fieldName"] = fk.reverseFieldName
100 fkmap[name[:-3]] = fkdict
Scott Bakeraa556b02017-03-07 16:07:34 -0800101 else:
102 # If there's a corresponding _type_id field, then see if this
103 # is a generic foreign key.
104 type_name = name[:-3] + "_type_id"
105 if type_name in all_field_names:
106 fkmap[name[:-3]] = {"src_fieldName": name, "ct_fieldName": type_name, "kind": "generic_fk"}
Scott Baker96b995a2017-02-15 16:21:12 -0800107
Scott Baker03a163f2017-05-17 09:21:47 -0700108 for gfk in self.get_generic_foreignkeys():
109 fkmap[gfk["name"]] = {"src_fieldName": gfk["id"], "ct_fieldName": gfk["content_type"], "kind": "generic_fk"}
110
Scott Baker96b995a2017-02-15 16:21:12 -0800111 return fkmap
112
113 def gen_reverse_fkmap(self):
114 reverse_fkmap = {}
115
116 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
117 if name.endswith("_ids"):
118 reverseForeignKey = field.GetOptions().Extensions._FindExtensionByName("xos.reverseForeignKey")
119 fk = field.GetOptions().Extensions[reverseForeignKey]
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800120 if fk and fk.modelName:
121 reverse_fkmap[name[:-4]] = {"src_fieldName": name, "modelName": fk.modelName, "writeable": False}
122 else:
123 manyToManyForeignKey = field.GetOptions().Extensions._FindExtensionByName("xos.manyToManyForeignKey")
124 fk = field.GetOptions().Extensions[manyToManyForeignKey]
125 if fk and fk.modelName:
126 reverse_fkmap[name[:-4]] = {"src_fieldName": name, "modelName": fk.modelName, "writeable": True}
127
Scott Baker96b995a2017-02-15 16:21:12 -0800128
129 return reverse_fkmap
130
131 def fk_resolve(self, name):
132 if name in self.cache:
Scott Bakerc4156c32017-12-08 10:58:21 -0800133 return self.cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800134
135 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800136 fk_kind = fk_entry["kind"]
137 fk_id = getattr(self, fk_entry["src_fieldName"])
138
139 if not fk_id:
140 return None
141
142 if fk_kind=="fk":
143 id=self.stub.make_ID(id=fk_id)
144 dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
145
146 elif fk_kind=="generic_fk":
Scott Bakerd2543ed2017-03-07 21:46:48 -0800147 dest_model = self.stub.genericForeignKeyResolve(getattr(self, fk_entry["ct_fieldName"]), fk_id)._wrapped_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800148
149 else:
150 raise Exception("unknown fk_kind")
Scott Baker96b995a2017-02-15 16:21:12 -0800151
Scott Bakerc4156c32017-12-08 10:58:21 -0800152 dest_model = make_ORMWrapper(dest_model, self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800153 self.cache[name] = dest_model
154
Scott Bakerc4156c32017-12-08 10:58:21 -0800155 return dest_model
Scott Baker96b995a2017-02-15 16:21:12 -0800156
157 def reverse_fk_resolve(self, name):
158 if name not in self.reverse_cache:
159 fk_entry = self._reverse_fkmap[name]
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800160 self.cache[name] = ORMLocalObjectManager(self.stub, fk_entry["modelName"], getattr(self, fk_entry["src_fieldName"]), fk_entry["writeable"])
Scott Baker96b995a2017-02-15 16:21:12 -0800161
162 return self.cache[name]
163
Scott Bakere72e7612017-02-20 10:07:09 -0800164 def fk_set(self, name, model):
165 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800166 fk_kind = fk_entry["kind"]
Scott Bakera1eae7a2017-06-06 09:20:15 -0700167 if model:
168 id = model.id
169 else:
170 id = 0
Scott Bakere72e7612017-02-20 10:07:09 -0800171 setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
172
Scott Bakeraa556b02017-03-07 16:07:34 -0800173 if fk_kind=="generic_fk":
174 setattr(self._wrapped_class, fk_entry["ct_fieldName"], model.self_content_type_id)
175
Scott Bakerc4156c32017-12-08 10:58:21 -0800176 if name in self.cache:
177 old_model = self.cache[name]
178 if fk_entry.get("reverse_fieldName"):
179 # Note this fk change so that we can update the destination model after we save.
180 self.post_save_fixups.append({"src_fieldName": fk_entry["src_fieldName"],
181 "dest_id": id,
182 "dest_model": old_model,
183 "remove": True,
184 "reverse_fieldName": fk_entry.get("reverse_fieldName")})
185 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800186
Scott Bakerc4156c32017-12-08 10:58:21 -0800187 if model:
188 self.cache[name] = model
189 if fk_entry.get("reverse_fieldName"):
190 # Note this fk change so that we can update the destination model after we save.
191 self.post_save_fixups.append({"src_fieldName": fk_entry["src_fieldName"],
192 "dest_id": id,
193 "dest_model": model,
194 "remove": False,
195 "reverse_fieldName": fk_entry.get("reverse_fieldName")})
196 elif name in self.cache:
197 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800198
Scott Bakerc4156c32017-12-08 10:58:21 -0800199 def do_post_save_fixups(self):
200 # Perform post-save foreign key fixups.
201 # Fixup the models that we've set a foreign key to so that their in-memory representation has the correct
202 # reverse foreign key back to us. We can only do this after a save, because self.id isn't known until
203 # after save.
204 # See unit test test_foreign_key_set_without_invalidate
205 for fixup in self.post_save_fixups:
206 model = fixup["dest_model"]
207 reverse_fieldName_ids = fixup["reverse_fieldName"] + "_ids"
208 if not hasattr(model, reverse_fieldName_ids):
209 continue
210 if fixup["remove"]:
211 reverse_ids = getattr(model, reverse_fieldName_ids)
212 if self.id in reverse_ids:
213 reverse_ids.remove(self.id)
214 else:
215 reverse_ids = getattr(model, reverse_fieldName_ids)
216 if self.id not in reverse_ids:
217 reverse_ids.append(self.id)
218 model.invalidate_cache(fixup["reverse_fieldName"])
219 self.post_save_fixups = []
Scott Bakere72e7612017-02-20 10:07:09 -0800220
Scott Baker96b995a2017-02-15 16:21:12 -0800221 def __getattr__(self, name, *args, **kwargs):
222 # note: getattr is only called for attributes that do not exist in
223 # self.__dict__
224
Scott Baker186372f2017-02-23 13:49:36 -0800225 # pk is a synonym for id
226 if (name == "pk"):
227 name = "id"
228
Scott Baker96b995a2017-02-15 16:21:12 -0800229 if name in self._fkmap.keys():
230 return self.fk_resolve(name)
231
232 if name in self._reverse_fkmap.keys():
233 return self.reverse_fk_resolve(name)
234
Scott Baker37cf9e22018-08-20 14:39:33 -0700235 try:
236 # When sending a reply, XOS will leave the field unset if it is None in the data model. If
237 # HasField(<fieldname>)==False for an existing object, then the caller can infer that field was set to
238 # None.
239 if (not self.is_new) and (not self._wrapped_class.HasField(name)):
240 return None
241 except ValueError:
242 # ValueError is thrown if the field does not exist. We will handle that case in the getattr() below.
243 pass
244
Scott Baker96b995a2017-02-15 16:21:12 -0800245 return getattr(self._wrapped_class, name, *args, **kwargs)
246
247 def __setattr__(self, name, value):
Scott Bakere72e7612017-02-20 10:07:09 -0800248 if name in self._fkmap.keys():
249 self.fk_set(name, value)
250 elif name in self.__dict__:
Scott Baker96b995a2017-02-15 16:21:12 -0800251 super(ORMWrapper,self).__setattr__(name, value)
Scott Baker37cf9e22018-08-20 14:39:33 -0700252 elif value is None:
253 # When handling requests, XOS interprets gRPC HasField(<fieldname>)==False to indicate that the caller
254 # has not set the field and wants it to continue to use its existing (or default) value. That leaves us
255 # with no easy way to support setting a field to None.
256 raise ValueError("Setting a non-foreignkey field to None is not supported")
Scott Baker96b995a2017-02-15 16:21:12 -0800257 else:
258 setattr(self._wrapped_class, name, value)
259
260 def __repr__(self):
Scott Bakerd1940972017-05-01 15:45:32 -0700261 class_name = self._wrapped_class.__class__.__name__
262 id = getattr(self._wrapped_class, "id", "noid")
263 name = getattr(self._wrapped_class, "name", None)
264 if name:
265 return "<%s: %s>" % (class_name, name)
266 else:
267 return "<%s: id-%s>" % (class_name, id)
268
269 def __str__(self):
270 class_name = self._wrapped_class.__class__.__name__
271 id = getattr(self._wrapped_class, "id", "noid")
272 name = getattr(self._wrapped_class, "name", None)
273 if name:
274 return name
275 else:
276 return "%s-%s" % (class_name, id)
277
278 def dumpstr(self):
Scott Baker96b995a2017-02-15 16:21:12 -0800279 return self._wrapped_class.__repr__()
280
Scott Bakerd1940972017-05-01 15:45:32 -0700281 def dump(self):
282 print self.dumpstr()
283
Scott Bakere72e7612017-02-20 10:07:09 -0800284 def invalidate_cache(self, name=None):
285 if name:
286 if name in self.cache:
287 del self.cache[name]
288 if name in self.reverse_cache:
289 del self.reverse_cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800290 else:
291 self.cache.clear()
292 self.reverse_cache.clear()
Scott Bakere72e7612017-02-20 10:07:09 -0800293
Scott Bakerd8246712018-07-12 18:08:31 -0700294 def save(self, update_fields=None, always_update_timestamp=False, is_sync_save=False, is_policy_save=False):
Scott Baker96b995a2017-02-15 16:21:12 -0800295 if self.is_new:
296 new_class = self.stub.invoke("Create%s" % self._wrapped_class.__class__.__name__, self._wrapped_class)
297 self._wrapped_class = new_class
298 self.is_new = False
299 else:
Scott Baker57c74822017-02-23 11:13:04 -0800300 metadata = []
301 if update_fields:
302 metadata.append( ("update_fields", ",".join(update_fields)) )
Scott Baker7b6aef02017-08-16 16:36:40 -0700303 if always_update_timestamp:
304 metadata.append( ("always_update_timestamp", "1") )
Scott Bakerd8246712018-07-12 18:08:31 -0700305 if is_policy_save:
306 metadata.append( ("is_policy_save", "1") )
307 if is_sync_save:
308 metadata.append( ("is_sync_save", "1") )
Scott Baker57c74822017-02-23 11:13:04 -0800309 self.stub.invoke("Update%s" % self._wrapped_class.__class__.__name__, self._wrapped_class, metadata=metadata)
Scott Bakerc4156c32017-12-08 10:58:21 -0800310 self.do_post_save_fixups()
Scott Baker96b995a2017-02-15 16:21:12 -0800311
312 def delete(self):
313 id = self.stub.make_ID(id=self._wrapped_class.id)
314 self.stub.invoke("Delete%s" % self._wrapped_class.__class__.__name__, id)
315
Scott Baker22796cc2017-02-23 16:53:34 -0800316 def tologdict(self):
317 try:
Sapan Bhatiae17c73a2017-07-26 23:44:34 -0400318 d = {'model_name':self._wrapped_class.__class__.__name__, 'pk': self.pk}
Scott Baker22796cc2017-02-23 16:53:34 -0800319 except:
320 d = {}
321
322 return d
323
Scott Bakerbb81e152017-03-02 15:28:36 -0800324 @property
Scott Bakerff104cc2017-08-14 15:24:41 -0700325 def leaf_model(self):
326 # Easy case - this model is already the leaf
327 if self.leaf_model_name == self._wrapped_class.__class__.__name__:
328 return self
329
330 # This model is not the leaf, so use the stub to fetch the leaf model
331 return getattr(self.stub, self.leaf_model_name).objects.get(id=self.id)
332
333 @property
Scott Bakerd2543ed2017-03-07 21:46:48 -0800334 def model_name(self):
335 return self._wrapped_class.__class__.__name__
336
337 @property
Scott Bakerbb81e152017-03-02 15:28:36 -0800338 def ansible_tag(self):
339 return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
340
Scott Bakerdd82b9a2017-03-09 14:49:55 -0800341# @property
342# def self_content_type_id(self):
343# return getattr(self.stub, self._wrapped_class.__class__.__name__).content_type_id
Scott Bakeraa556b02017-03-07 16:07:34 -0800344
Scott Bakerb05393b2017-03-01 14:59:55 -0800345class ORMQuerySet(list):
346 """ Makes lists look like django querysets """
347 def first(self):
348 if len(self)>0:
349 return self[0]
350 else:
351 return None
352
Scott Baker8c7267d2017-03-14 19:34:13 -0700353 def exists(self):
354 return len(self)>0
355
Scott Baker96b995a2017-02-15 16:21:12 -0800356class ORMLocalObjectManager(object):
357 """ Manages a local list of objects """
358
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800359 def __init__(self, stub, modelName, idList, writeable):
Scott Baker96b995a2017-02-15 16:21:12 -0800360 self._stub = stub
361 self._modelName = modelName
362 self._idList = idList
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800363 self._writeable = writeable
Scott Baker96b995a2017-02-15 16:21:12 -0800364 self._cache = None
365
366 def resolve_queryset(self):
367 if self._cache is not None:
368 return self._cache
369
370 models = []
371 for id in self._idList:
372 models.append(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id)))
373
374 self._cache = models
375
376 return models
377
378 def all(self):
379 models = self.resolve_queryset()
Scott Baker22796cc2017-02-23 16:53:34 -0800380 return [make_ORMWrapper(x,self._stub) for x in models]
Scott Baker96b995a2017-02-15 16:21:12 -0800381
Scott Baker8c7267d2017-03-14 19:34:13 -0700382 def exists(self):
383 return len(self._idList)>0
384
Scott Bakera1eae7a2017-06-06 09:20:15 -0700385 def count(self):
386 return len(self._idList)
387
Scott Baker8c7267d2017-03-14 19:34:13 -0700388 def first(self):
389 if self._idList:
390 model = make_ORMWrapper(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=self._idList[0])), self._stub)
391 return model
392 else:
393 return None
394
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800395 def add(self, model):
396 if not self._writeable:
397 raise Exception("Only ManyToMany lists are writeable")
398
399 if isinstance(model, int):
400 id = model
401 else:
402 if not model.id:
403 raise Exception("Model %s has no id" % model)
404 id = model.id
405
406 self._idList.append(id)
407
408 def remove(self, model):
409 if not self._writeable:
410 raise Exception("Only ManyToMany lists are writeable")
411
412 if isinstance(model, int):
413 id = model
414 else:
415 if not model.id:
416 raise Exception("Model %s has no id" % model)
417 id = model.id
418
419 self._idList.remove(id)
420
Scott Baker96b995a2017-02-15 16:21:12 -0800421class ORMObjectManager(object):
422 """ Manages a remote list of objects """
423
Scott Bakerac2f2b52017-02-21 14:53:23 -0800424 # constants better agree with common.proto
Scott Bakerbae9d842017-03-21 10:44:10 -0700425 SYNCHRONIZER_DIRTY_OBJECTS = 2
426 SYNCHRONIZER_DELETED_OBJECTS = 3
427 SYNCHRONIZER_DIRTY_POLICIES = 4
428 SYNCHRONIZER_DELETED_POLICIES = 5
Scott Bakerac2f2b52017-02-21 14:53:23 -0800429
Scott Baker96b995a2017-02-15 16:21:12 -0800430 def __init__(self, stub, modelName, packageName):
431 self._stub = stub
432 self._modelName = modelName
433 self._packageName = packageName
434
435 def wrap_single(self, obj):
Scott Baker22796cc2017-02-23 16:53:34 -0800436 return make_ORMWrapper(obj, self._stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800437
438 def wrap_list(self, obj):
439 result=[]
440 for item in obj.items:
Scott Baker22796cc2017-02-23 16:53:34 -0800441 result.append(make_ORMWrapper(item, self._stub))
Scott Bakerb05393b2017-03-01 14:59:55 -0800442 return ORMQuerySet(result)
Scott Baker96b995a2017-02-15 16:21:12 -0800443
444 def all(self):
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700445 return self.wrap_list(self._stub.invoke("List%s" % self._modelName, self._stub.make_empty()))
Scott Baker96b995a2017-02-15 16:21:12 -0800446
Scott Baker22796cc2017-02-23 16:53:34 -0800447 def first(self):
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700448 objs=self.wrap_list(self._stub.invoke("List%s" % self._modelName, self._stub.make_empty()))
Scott Baker22796cc2017-02-23 16:53:34 -0800449 if not objs:
450 return None
451 return objs[0]
452
Scott Bakerac2f2b52017-02-21 14:53:23 -0800453 def filter(self, **kwargs):
454 q = self._stub.make_Query()
455 q.kind = q.DEFAULT
456
457 for (name, val) in kwargs.items():
458 el = q.elements.add()
459
460 if name.endswith("__gt"):
461 name = name[:-4]
462 el.operator = el.GREATER_THAN
463 elif name.endswith("__gte"):
464 name = name[:-5]
465 el.operator = el.GREATER_THAN_OR_EQUAL
466 elif name.endswith("__lt"):
467 name = name[:-4]
468 el.operator = el.LESS_THAN
469 elif name.endswith("__lte"):
470 name = name[:-5]
471 el.operator = el.LESS_THAN_OR_EQUAL
Scott Bakere1607b82018-09-20 14:10:59 -0700472 elif name.endswith("__iexact"):
473 name = name[:-8]
474 el.operator = el.IEXACT
Scott Bakerac2f2b52017-02-21 14:53:23 -0800475 else:
476 el.operator = el.EQUAL
477
478 el.name = name
479 if isinstance(val, int):
480 el.iValue = val
481 else:
482 el.sValue = val
483
484 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
485
486 def filter_special(self, kind):
487 q = self._stub.make_Query()
488 q.kind = kind
489 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
490
Scott Baker22796cc2017-02-23 16:53:34 -0800491 def get(self, **kwargs):
492 if kwargs.keys() == ["id"]:
493 # the fast and easy case, look it up by id
494 return self.wrap_single(self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=kwargs["id"])))
495 else:
496 # the slightly more difficult case, filter and return the first item
497 objs = self.filter(**kwargs)
498 return objs[0]
Scott Baker96b995a2017-02-15 16:21:12 -0800499
500 def new(self, **kwargs):
Scott Bakeraa556b02017-03-07 16:07:34 -0800501 cls = self._stub.all_grpc_classes[self._modelName]
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700502 o = make_ORMWrapper(cls(), self._stub, is_new=True)
503 for (k,v) in kwargs.items():
504 setattr(o, k, v)
505 return o
Scott Baker96b995a2017-02-15 16:21:12 -0800506
507class ORMModelClass(object):
508 def __init__(self, stub, model_name, package_name):
Scott Baker22796cc2017-02-23 16:53:34 -0800509 self.model_name = model_name
Scott Bakeraa556b02017-03-07 16:07:34 -0800510 self._stub = stub
Scott Baker96b995a2017-02-15 16:21:12 -0800511 self.objects = ORMObjectManager(stub, model_name, package_name)
512
Scott Bakerbb81e152017-03-02 15:28:36 -0800513 @property
Scott Baker22796cc2017-02-23 16:53:34 -0800514 def __name__(self):
515 return self.model_name
516
Scott Bakeraa556b02017-03-07 16:07:34 -0800517 @property
518 def content_type_id(self):
519 return self._stub.reverse_content_type_map[self.model_name]
520
Scott Baker8a6d91f2017-03-22 11:23:11 -0700521 def __call__(self, *args, **kwargs):
522 return self.objects.new(*args, **kwargs)
523
Scott Baker96b995a2017-02-15 16:21:12 -0800524class ORMStub(object):
Scott Bakerb96ba432018-02-26 09:53:48 -0800525 def __init__(self, stub, protos, package_name, invoker=None, caller_kind="grpcapi", empty = None,
Scott Bakerb06e3e02017-12-12 11:05:53 -0800526 enable_backoff=True, restart_on_disconnect=False):
Scott Baker96b995a2017-02-15 16:21:12 -0800527 self.grpc_stub = stub
Scott Bakerb96ba432018-02-26 09:53:48 -0800528 self.protos = protos
529 self.common_protos = protos.common__pb2
Scott Baker96b995a2017-02-15 16:21:12 -0800530 self.all_model_names = []
Scott Bakeraa556b02017-03-07 16:07:34 -0800531 self.all_grpc_classes = {}
532 self.content_type_map = {}
533 self.reverse_content_type_map = {}
Scott Bakeref8d85d2017-02-21 16:44:28 -0800534 self.invoker = invoker
Scott Baker22796cc2017-02-23 16:53:34 -0800535 self.caller_kind = caller_kind
Scott Baker500f8c72017-05-19 09:41:50 -0700536 self.enable_backoff = enable_backoff
Scott Bakerb06e3e02017-12-12 11:05:53 -0800537 self.restart_on_disconnect = restart_on_disconnect
Scott Baker96b995a2017-02-15 16:21:12 -0800538
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700539 if not empty:
Scott Bakerb96ba432018-02-26 09:53:48 -0800540 empty = self.protos.google_dot_protobuf_dot_empty__pb2.Empty
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700541 self._empty = empty
542
Scott Baker96b995a2017-02-15 16:21:12 -0800543 for name in dir(stub):
544 if name.startswith("Get"):
545 model_name = name[3:]
546 setattr(self,model_name, ORMModelClass(self, model_name, package_name))
547
548 self.all_model_names.append(model_name)
549
Scott Bakerb96ba432018-02-26 09:53:48 -0800550 grpc_class = getattr(self.protos, model_name)
Scott Bakeraa556b02017-03-07 16:07:34 -0800551 self.all_grpc_classes[model_name] = grpc_class
552
553 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions._FindExtensionByName("xos.contentTypeId")
554 if ct:
555 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions[ct]
556 if ct:
557 self.content_type_map[ct] = model_name
558 self.reverse_content_type_map[model_name] = ct
559
560 def genericForeignKeyResolve(self, content_type_id, id):
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700561 if content_type_id.endswith("_decl"):
562 content_type_id = content_type_id[:-5]
563
564 if content_type_id not in self.content_type_map:
565 raise ORMGenericContentNotFoundException("Content_type %s not found in self.content_type_map" % content_type_id)
566
Scott Bakeraa556b02017-03-07 16:07:34 -0800567 model_name = self.content_type_map[content_type_id]
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700568
Scott Bakeraa556b02017-03-07 16:07:34 -0800569 model = getattr(self, model_name)
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700570 objs = model.objects.filter(id=id)
571 if not objs:
572 raise ORMGenericObjectNotFoundException("Object %s of model %s was not found" % (id,model_name))
573
Scott Bakeraa556b02017-03-07 16:07:34 -0800574 return model.objects.get(id=id)
575
Scott Baker22796cc2017-02-23 16:53:34 -0800576 def add_default_metadata(self, metadata):
577 default_metadata = [ ("caller_kind", self.caller_kind) ]
578
Scott Bakerd8246712018-07-12 18:08:31 -0700579 # introspect to see if we're running from a synchronizer thread
580 if getattr(threading.current_thread(), "is_sync_thread", False):
581 default_metadata.append( ("is_sync_save", "1") )
582
583 # introspect to see if we're running from a model_policy thread
584 if getattr(threading.current_thread(), "is_policy_thread", False):
585 default_metadata.append( ("is_policy_save", "1") )
586
Scott Baker22796cc2017-02-23 16:53:34 -0800587 # build up a list of metadata keys we already have
588 md_keys=[x[0] for x in metadata]
589
590 # add any defaults that we don't already have
591 for md in default_metadata:
592 if md[0] not in md_keys:
593 metadata.append( (md[0], md[1]) )
594
Scott Baker57c74822017-02-23 11:13:04 -0800595 def invoke(self, name, request, metadata=[]):
Scott Baker22796cc2017-02-23 16:53:34 -0800596 self.add_default_metadata(metadata)
597
Scott Bakeref8d85d2017-02-21 16:44:28 -0800598 if self.invoker:
599 # Hook in place to call Chameleon's invoke method, as soon as we
600 # have rewritten the synchronizer to use reactor.
601 return self.invoker.invoke(self.grpc_stub.__class__, name, request, metadata={}).result[0]
Scott Baker500f8c72017-05-19 09:41:50 -0700602 elif self.enable_backoff:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800603 # Our own retry mechanism. This works fine if there is a temporary
604 # failure in connectivity, but does not re-download gRPC schema.
Scott Baker500f8c72017-05-19 09:41:50 -0700605 import grpc
Scott Bakerb06e3e02017-12-12 11:05:53 -0800606 backoff = [0.5, 1, 2, 4, 8]
Scott Bakeref8d85d2017-02-21 16:44:28 -0800607 while True:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800608 try:
609 method = getattr(self.grpc_stub, name)
Scott Baker57c74822017-02-23 11:13:04 -0800610 return method(request, metadata=metadata)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800611 except grpc._channel._Rendezvous, e:
612 code = e.code()
613 if code == grpc.StatusCode.UNAVAILABLE:
Scott Bakerb06e3e02017-12-12 11:05:53 -0800614 if self.restart_on_disconnect:
615 # This is a blunt technique... We lost connectivity to the core, and we don't know that
616 # the core is still serving up the same models it was when we established connectivity,
617 # so restart the synchronizer.
618 # TODO: Hash check on the core models to tell if something changed would be better.
619 os.execv(sys.executable, ['python'] + sys.argv)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800620 if not backoff:
621 raise Exception("No more retries on %s" % name)
622 time.sleep(backoff.pop(0))
623 else:
624 raise
625 except:
626 raise
Scott Baker500f8c72017-05-19 09:41:50 -0700627 else:
628 method = getattr(self.grpc_stub, name)
629 return method(request, metadata=metadata)
630
Scott Baker96b995a2017-02-15 16:21:12 -0800631
632 def make_ID(self, id):
Scott Bakerb96ba432018-02-26 09:53:48 -0800633 return getattr(self.common_protos, "ID")(id=id)
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700634
635 def make_empty(self):
636 return self._empty()
Scott Baker96b995a2017-02-15 16:21:12 -0800637
Scott Bakerac2f2b52017-02-21 14:53:23 -0800638 def make_Query(self):
Scott Bakerb96ba432018-02-26 09:53:48 -0800639 return getattr(self.common_protos, "Query")()
Scott Bakerac2f2b52017-02-21 14:53:23 -0800640
Scott Bakerf6145a22017-03-29 14:50:25 -0700641 def listObjects(self):
642 return self.all_model_names
643
Scott Baker22796cc2017-02-23 16:53:34 -0800644def register_convenience_wrapper(class_name, wrapper):
645 global convenience_wrappers
Scott Baker96b995a2017-02-15 16:21:12 -0800646
Scott Baker22796cc2017-02-23 16:53:34 -0800647 convenience_wrappers[class_name] = wrapper
648
649def make_ORMWrapper(wrapped_class, *args, **kwargs):
Scott Baker2f314d52018-08-24 08:31:19 -0700650 cls = None
651
652 if (not cls) and wrapped_class.__class__.__name__ in convenience_wrappers:
Scott Baker22796cc2017-02-23 16:53:34 -0800653 cls = convenience_wrappers[wrapped_class.__class__.__name__]
Scott Baker2f314d52018-08-24 08:31:19 -0700654
655 if (not cls):
656 # Search the list of class names for this model to see if we have any applicable wrappers. The list is always
657 # sorted from most specific to least specific, so the first one we find will automatically be the most relevant
658 # one. If we don't find any, then default to ORMWrapper
659
660 # Note: Only works on objects that have been fetched from the server, not objects that are created on the
661 # client. This is because wrapped_class.class_names is filled in by the server.
662
663 # TODO(smbaker): Ought to be able to make this work with newly created objects after they are saved.
664
665 for name in wrapped_class.class_names.split(","):
666 if name in convenience_wrappers:
667 cls = convenience_wrappers[name]
668
669 if (not cls):
Scott Baker22796cc2017-02-23 16:53:34 -0800670 cls = ORMWrapper
671
672 return cls(wrapped_class, *args, **kwargs)
673
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200674def import_convenience_methods():
675
676 log.info("Loading convenience methods")
677
678 cwd = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
679 api_convenience_dir = os.path.join(cwd, "convenience")
680 for file in os.listdir(api_convenience_dir):
681 if file.endswith(".py") and not "test" in file:
682 pathname = os.path.join(api_convenience_dir, file)
683 try:
684 log.debug("Loading: %s" % file)
685 imp.load_source(file[:-3], pathname)
686 except Exception, e:
Matteo Scandolo31361ae2018-05-21 16:09:02 -0700687 log.exception("Cannot import api convenience method for: %s, %s" % (file[:-3], pathname))
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200688
689
690# import convenience.addresspool
691# import convenience.privilege
692# import convenience.instance
693# import convenience.network
694# import convenience.cordsubscriberroot
695# import convenience.vsgserviceinstance
696# import convenience.serviceinstance
697# import convenience.vrouterservice
698# import convenience.vroutertenant
699# import convenience.vrouterapp
700# import convenience.service
701# import convenience.onosapp
702# import convenience.controller
703# import convenience.user
704# import convenience.slice
705# import convenience.port
706# import convenience.tag
707# import convenience.vtrtenant
708# import convenience.addressmanagerservice
709# import convenience.addressmanagerserviceinstance
Scott Baker500f8c72017-05-19 09:41:50 -0700710