blob: 18c473d8cf732cca21f611b1f1d5549f5a0b674e [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001# Copyright 2017-present Open Networking Foundation
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
Zack Williams5c2ea232019-01-30 15:23:01 -070015from __future__ import absolute_import, print_function
16
17import imp
Zack Williams045b63d2019-01-22 16:30:57 -070018import os
19import sys
20import threading
21import time
Andy Bavier04ee1912019-01-30 14:17:16 -070022import traceback
Zack Williams5c2ea232019-01-30 15:23:01 -070023
Zack Williams045b63d2019-01-22 16:30:57 -070024from multistructlog import create_logger
Zack Williams5c2ea232019-01-30 15:23:01 -070025from xosconfig import Config
Matteo Scandolod2044a42017-08-07 16:08:28 -070026
Scott Baker96b995a2017-02-15 16:21:12 -080027"""
28Django-like ORM layer for gRPC
29
30Usage:
31 api = ORMStub(stub)
32
33 api.Slices.all() ... list all slices
34
35 someSlice = api.Slices.get(id=1) ... get slice #1
36
37 someSlice.site ... automatically resolves site_id into a site object
38 someSlice.instances ... automatically resolves instances_ids into instance objects
39 someSlice.save() ... saves the slice object
40"""
41
42"""
43import grpc_client, orm
44c=grpc_client.SecureClient("xos-core.cord.lab", username="padmin@vicci.org", password="letmein")
Scott Bakera1eae7a2017-06-06 09:20:15 -070045u=c.xos_orm.User.objects.get(id=1)
46"""
Scott Baker96b995a2017-02-15 16:21:12 -080047
Zack Williams045b63d2019-01-22 16:30:57 -070048log = create_logger(Config().get("logging"))
Scott Baker96b995a2017-02-15 16:21:12 -080049
Scott Baker22796cc2017-02-23 16:53:34 -080050convenience_wrappers = {}
51
Zack Williams5c2ea232019-01-30 15:23:01 -070052
Andy Bavier04ee1912019-01-30 14:17:16 -070053def get_synchronizer_function():
Zack Williams5c2ea232019-01-30 15:23:01 -070054 """
55 Find the topmost synchronizer-specific function in the call stack
56 """
Andy Bavier04ee1912019-01-30 14:17:16 -070057 result = None
Zack Williams5c2ea232019-01-30 15:23:01 -070058 for file, line, func, stmt in traceback.extract_stack():
Andy Bavier04ee1912019-01-30 14:17:16 -070059 if file.startswith("/opt/xos/synchronizers"):
60 if not result:
Zack Williams5c2ea232019-01-30 15:23:01 -070061 result = "%s:%s()" % (file, func)
Andy Bavier04ee1912019-01-30 14:17:16 -070062 if not file.startswith("/opt/xos/synchronizers/new_base"):
Zack Williams5c2ea232019-01-30 15:23:01 -070063 result = "%s:%s()" % (file, func)
Andy Bavier04ee1912019-01-30 14:17:16 -070064 break
65 return result
Zack Williams045b63d2019-01-22 16:30:57 -070066
Zack Williams5c2ea232019-01-30 15:23:01 -070067
Scott Bakerd0f1dc12018-04-23 12:05:32 -070068class ORMGenericContentNotFoundException(Exception):
69 pass
70
Zack Williams045b63d2019-01-22 16:30:57 -070071
Scott Bakerd0f1dc12018-04-23 12:05:32 -070072class ORMGenericObjectNotFoundException(Exception):
73 pass
74
Zack Williams045b63d2019-01-22 16:30:57 -070075
Scott Baker96b995a2017-02-15 16:21:12 -080076class ORMWrapper(object):
77 """ Wraps a protobuf object to provide ORM features """
78
79 def __init__(self, wrapped_class, stub, is_new=False):
80 super(ORMWrapper, self).__setattr__("_wrapped_class", wrapped_class)
81 super(ORMWrapper, self).__setattr__("stub", stub)
82 super(ORMWrapper, self).__setattr__("cache", {})
83 super(ORMWrapper, self).__setattr__("reverse_cache", {})
Sapan Bhatia71f57682017-08-23 20:09:08 -040084 super(ORMWrapper, self).__setattr__("synchronizer_step", None)
Sapan Bhatia2b307f72017-11-02 11:39:17 -040085 super(ORMWrapper, self).__setattr__("dependent", None)
Scott Baker96b995a2017-02-15 16:21:12 -080086 super(ORMWrapper, self).__setattr__("is_new", is_new)
Scott Bakerc4156c32017-12-08 10:58:21 -080087 super(ORMWrapper, self).__setattr__("post_save_fixups", [])
Zack Williams045b63d2019-01-22 16:30:57 -070088 fkmap = self.gen_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080089 super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
Zack Williams045b63d2019-01-22 16:30:57 -070090 reverse_fkmap = self.gen_reverse_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080091 super(ORMWrapper, self).__setattr__("_reverse_fkmap", reverse_fkmap)
Scott Baker5b7fba02018-10-17 08:46:46 -070092 super(ORMWrapper, self).__setattr__("_initial", self._dict)
93
Zack Williams045b63d2019-01-22 16:30:57 -070094 def fields_differ(self, f1, f2):
95 return f1 != f2
Scott Baker5b7fba02018-10-17 08:46:46 -070096
97 @property
98 def _dict(self):
99 """ Return a dictionary of {fieldname: fieldvalue} for the object.
100
101 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
102 that are set to default values. ORM ignores fields that are set to default values.
103 """
Zack Williams045b63d2019-01-22 16:30:57 -0700104 d = {}
Scott Baker5b7fba02018-10-17 08:46:46 -0700105 for (fieldDesc, val) in self._wrapped_class.ListFields():
106 name = fieldDesc.name
107 d[name] = val
108 return d
109
110 @property
111 def diff(self):
112 d1 = self._initial
113 d2 = self._dict
Zack Williams5c2ea232019-01-30 15:23:01 -0700114 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Zack Williams045b63d2019-01-22 16:30:57 -0700115 diffs = []
Scott Baker5b7fba02018-10-17 08:46:46 -0700116 for k in all_field_names:
Zack Williams045b63d2019-01-22 16:30:57 -0700117 if d1.get(k, None) != d2.get(k, None):
118 diffs.append((k, (d1.get(k, None), d2.get(k, None))))
Scott Baker5b7fba02018-10-17 08:46:46 -0700119
Zack Williams045b63d2019-01-22 16:30:57 -0700120 # diffs = [(k, (v, d2[k])) for k, v in d1.items() if self.fields_differ(v,d2[k])]
Scott Baker5b7fba02018-10-17 08:46:46 -0700121 return dict(diffs)
122
123 @property
124 def has_changed(self):
125 return bool(self.diff)
126
127 @property
128 def changed_fields(self):
129 """ Return the list of changed fields.
130
131 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
132 that are set to default values.
133 """
134 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700135 return list(self._dict.keys())
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700136
Zack Williams5c2ea232019-01-30 15:23:01 -0700137 return list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700138
139 def has_field_changed(self, field_name):
Zack Williams5c2ea232019-01-30 15:23:01 -0700140 return field_name in list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700141
142 def get_field_diff(self, field_name):
143 return self.diff.get(field_name, None)
144
145 def recompute_initial(self):
146 self._initial = self._dict
147
148 def save_changed_fields(self, always_update_timestamp=False):
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700149
150 # we need to ignore many-to-many fields as they are saved by do_post_save_fixups
151 # and can't be sent over the wire
152 m2m_fields = [v['src_fieldName'] for k, v in self._reverse_fkmap.items()]
153
Scott Baker5b7fba02018-10-17 08:46:46 -0700154 if self.has_changed:
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700155 update_fields = [f for f in self.changed_fields if f not in m2m_fields]
Scott Baker5b7fba02018-10-17 08:46:46 -0700156 if always_update_timestamp and "updated" not in update_fields:
157 update_fields.append("updated")
Zack Williams045b63d2019-01-22 16:30:57 -0700158 self.save(
159 update_fields=sorted(update_fields),
160 always_update_timestamp=always_update_timestamp,
161 )
Scott Baker96b995a2017-02-15 16:21:12 -0800162
Scott Bakerd78f6472017-03-14 17:30:14 -0700163 def create_attr(self, name, value=None):
164 """ setattr(self, ...) will fail for attributes that don't exist in the
165 wrapped grpc class. This is by design. However, if someone really
166 does want to attach a new attribute to this class, then they can
167 call create_attr()
168 """
169 super(ORMWrapper, self).__setattr__(name, value)
170
Scott Baker03a163f2017-05-17 09:21:47 -0700171 def get_generic_foreignkeys(self):
172 """ this is a placeholder until generic foreign key support is added
173 to xproto.
174 """
175 return []
176
Scott Baker96b995a2017-02-15 16:21:12 -0800177 def gen_fkmap(self):
178 fkmap = {}
179
Zack Williams5c2ea232019-01-30 15:23:01 -0700180 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Scott Bakeraa556b02017-03-07 16:07:34 -0800181
Scott Baker96b995a2017-02-15 16:21:12 -0800182 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700183 if name.endswith("_id"):
184 foreignKey = field.GetOptions().Extensions._FindExtensionByName(
185 "xos.foreignKey"
186 )
187 fk = field.GetOptions().Extensions[foreignKey]
188 if fk and fk.modelName:
189 fkdict = {
190 "src_fieldName": name,
191 "modelName": fk.modelName,
192 "kind": "fk",
193 }
194 if fk.reverseFieldName:
195 fkdict["reverse_fieldName"] = fk.reverseFieldName
196 fkmap[name[:-3]] = fkdict
197 else:
198 # If there's a corresponding _type_id field, then see if this
199 # is a generic foreign key.
200 type_name = name[:-3] + "_type_id"
201 if type_name in all_field_names:
202 fkmap[name[:-3]] = {
203 "src_fieldName": name,
204 "ct_fieldName": type_name,
205 "kind": "generic_fk",
206 }
Scott Baker96b995a2017-02-15 16:21:12 -0800207
Scott Baker03a163f2017-05-17 09:21:47 -0700208 for gfk in self.get_generic_foreignkeys():
Zack Williams045b63d2019-01-22 16:30:57 -0700209 fkmap[gfk["name"]] = {
210 "src_fieldName": gfk["id"],
211 "ct_fieldName": gfk["content_type"],
212 "kind": "generic_fk",
213 }
Scott Baker03a163f2017-05-17 09:21:47 -0700214
Scott Baker96b995a2017-02-15 16:21:12 -0800215 return fkmap
216
217 def gen_reverse_fkmap(self):
218 reverse_fkmap = {}
219
220 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700221 if name.endswith("_ids"):
222 reverseForeignKey = field.GetOptions().Extensions._FindExtensionByName(
223 "xos.reverseForeignKey"
224 )
225 fk = field.GetOptions().Extensions[reverseForeignKey]
226 if fk and fk.modelName:
227 reverse_fkmap[name[:-4]] = {
228 "src_fieldName": name,
229 "modelName": fk.modelName,
230 "writeable": False,
231 }
232 else:
233 manyToManyForeignKey = field.GetOptions().Extensions._FindExtensionByName(
234 "xos.manyToManyForeignKey"
235 )
236 fk = field.GetOptions().Extensions[manyToManyForeignKey]
237 if fk and fk.modelName:
238 reverse_fkmap[name[:-4]] = {
239 "src_fieldName": name,
240 "modelName": fk.modelName,
241 "writeable": True,
242 }
Scott Baker96b995a2017-02-15 16:21:12 -0800243
244 return reverse_fkmap
245
246 def fk_resolve(self, name):
247 if name in self.cache:
Scott Bakerc4156c32017-12-08 10:58:21 -0800248 return self.cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800249
250 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800251 fk_kind = fk_entry["kind"]
252 fk_id = getattr(self, fk_entry["src_fieldName"])
253
254 if not fk_id:
255 return None
256
Zack Williams045b63d2019-01-22 16:30:57 -0700257 if fk_kind == "fk":
258 id = self.stub.make_ID(id=fk_id)
Scott Bakeraa556b02017-03-07 16:07:34 -0800259 dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
260
Zack Williams045b63d2019-01-22 16:30:57 -0700261 elif fk_kind == "generic_fk":
262 dest_model = self.stub.genericForeignKeyResolve(
263 getattr(self, fk_entry["ct_fieldName"]), fk_id
264 )._wrapped_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800265
266 else:
267 raise Exception("unknown fk_kind")
Scott Baker96b995a2017-02-15 16:21:12 -0800268
Scott Bakerc4156c32017-12-08 10:58:21 -0800269 dest_model = make_ORMWrapper(dest_model, self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800270 self.cache[name] = dest_model
271
Scott Bakerc4156c32017-12-08 10:58:21 -0800272 return dest_model
Scott Baker96b995a2017-02-15 16:21:12 -0800273
274 def reverse_fk_resolve(self, name):
275 if name not in self.reverse_cache:
276 fk_entry = self._reverse_fkmap[name]
Zack Williams045b63d2019-01-22 16:30:57 -0700277 self.reverse_cache[name] = ORMLocalObjectManager(
278 self.stub,
279 fk_entry["modelName"],
280 getattr(self, fk_entry["src_fieldName"]),
281 fk_entry["writeable"],
282 )
Scott Baker96b995a2017-02-15 16:21:12 -0800283
Scott Baker7ab456b2019-01-08 14:58:13 -0800284 return self.reverse_cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800285
Scott Bakere72e7612017-02-20 10:07:09 -0800286 def fk_set(self, name, model):
287 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800288 fk_kind = fk_entry["kind"]
Scott Bakera1eae7a2017-06-06 09:20:15 -0700289 if model:
290 id = model.id
291 else:
292 id = 0
Scott Bakere72e7612017-02-20 10:07:09 -0800293 setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
294
Zack Williams045b63d2019-01-22 16:30:57 -0700295 if fk_kind == "generic_fk":
296 setattr(
297 self._wrapped_class,
298 fk_entry["ct_fieldName"],
299 model.self_content_type_id,
300 )
Scott Bakeraa556b02017-03-07 16:07:34 -0800301
Scott Bakerc4156c32017-12-08 10:58:21 -0800302 if name in self.cache:
303 old_model = self.cache[name]
304 if fk_entry.get("reverse_fieldName"):
305 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700306 self.post_save_fixups.append(
307 {
308 "src_fieldName": fk_entry["src_fieldName"],
309 "dest_id": id,
310 "dest_model": old_model,
311 "remove": True,
312 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
313 }
314 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800315 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800316
Scott Bakerc4156c32017-12-08 10:58:21 -0800317 if model:
318 self.cache[name] = model
319 if fk_entry.get("reverse_fieldName"):
320 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700321 self.post_save_fixups.append(
322 {
323 "src_fieldName": fk_entry["src_fieldName"],
324 "dest_id": id,
325 "dest_model": model,
326 "remove": False,
327 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
328 }
329 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800330 elif name in self.cache:
331 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800332
Scott Bakerc4156c32017-12-08 10:58:21 -0800333 def do_post_save_fixups(self):
334 # Perform post-save foreign key fixups.
335 # Fixup the models that we've set a foreign key to so that their in-memory representation has the correct
336 # reverse foreign key back to us. We can only do this after a save, because self.id isn't known until
337 # after save.
338 # See unit test test_foreign_key_set_without_invalidate
339 for fixup in self.post_save_fixups:
340 model = fixup["dest_model"]
341 reverse_fieldName_ids = fixup["reverse_fieldName"] + "_ids"
342 if not hasattr(model, reverse_fieldName_ids):
343 continue
344 if fixup["remove"]:
345 reverse_ids = getattr(model, reverse_fieldName_ids)
346 if self.id in reverse_ids:
347 reverse_ids.remove(self.id)
348 else:
349 reverse_ids = getattr(model, reverse_fieldName_ids)
350 if self.id not in reverse_ids:
351 reverse_ids.append(self.id)
352 model.invalidate_cache(fixup["reverse_fieldName"])
353 self.post_save_fixups = []
Scott Bakere72e7612017-02-20 10:07:09 -0800354
Scott Baker96b995a2017-02-15 16:21:12 -0800355 def __getattr__(self, name, *args, **kwargs):
356 # note: getattr is only called for attributes that do not exist in
357 # self.__dict__
358
Scott Baker186372f2017-02-23 13:49:36 -0800359 # pk is a synonym for id
Zack Williams045b63d2019-01-22 16:30:57 -0700360 if name == "pk":
Scott Baker186372f2017-02-23 13:49:36 -0800361 name = "id"
362
Zack Williams5c2ea232019-01-30 15:23:01 -0700363 if name in list(self._fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800364 return self.fk_resolve(name)
365
Zack Williams5c2ea232019-01-30 15:23:01 -0700366 if name in list(self._reverse_fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800367 return self.reverse_fk_resolve(name)
368
Scott Baker37cf9e22018-08-20 14:39:33 -0700369 try:
370 # When sending a reply, XOS will leave the field unset if it is None in the data model. If
371 # HasField(<fieldname>)==False for an existing object, then the caller can infer that field was set to
372 # None.
373 if (not self.is_new) and (not self._wrapped_class.HasField(name)):
374 return None
375 except ValueError:
376 # ValueError is thrown if the field does not exist. We will handle that case in the getattr() below.
377 pass
378
Scott Baker96b995a2017-02-15 16:21:12 -0800379 return getattr(self._wrapped_class, name, *args, **kwargs)
380
381 def __setattr__(self, name, value):
Zack Williams5c2ea232019-01-30 15:23:01 -0700382 if name in list(self._fkmap.keys()):
Scott Bakere72e7612017-02-20 10:07:09 -0800383 self.fk_set(name, value)
384 elif name in self.__dict__:
Zack Williams045b63d2019-01-22 16:30:57 -0700385 super(ORMWrapper, self).__setattr__(name, value)
Scott Baker37cf9e22018-08-20 14:39:33 -0700386 elif value is None:
387 # When handling requests, XOS interprets gRPC HasField(<fieldname>)==False to indicate that the caller
388 # has not set the field and wants it to continue to use its existing (or default) value. That leaves us
389 # with no easy way to support setting a field to None.
390 raise ValueError("Setting a non-foreignkey field to None is not supported")
Scott Baker96b995a2017-02-15 16:21:12 -0800391 else:
392 setattr(self._wrapped_class, name, value)
393
394 def __repr__(self):
Scott Bakerd1940972017-05-01 15:45:32 -0700395 class_name = self._wrapped_class.__class__.__name__
396 id = getattr(self._wrapped_class, "id", "noid")
397 name = getattr(self._wrapped_class, "name", None)
398 if name:
399 return "<%s: %s>" % (class_name, name)
400 else:
401 return "<%s: id-%s>" % (class_name, id)
402
403 def __str__(self):
404 class_name = self._wrapped_class.__class__.__name__
405 id = getattr(self._wrapped_class, "id", "noid")
406 name = getattr(self._wrapped_class, "name", None)
407 if name:
408 return name
409 else:
410 return "%s-%s" % (class_name, id)
411
412 def dumpstr(self):
Scott Baker96b995a2017-02-15 16:21:12 -0800413 return self._wrapped_class.__repr__()
414
Scott Bakerd1940972017-05-01 15:45:32 -0700415 def dump(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700416 print(self.dumpstr())
Scott Bakerd1940972017-05-01 15:45:32 -0700417
Scott Bakere72e7612017-02-20 10:07:09 -0800418 def invalidate_cache(self, name=None):
419 if name:
420 if name in self.cache:
421 del self.cache[name]
422 if name in self.reverse_cache:
423 del self.reverse_cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800424 else:
425 self.cache.clear()
426 self.reverse_cache.clear()
Scott Bakere72e7612017-02-20 10:07:09 -0800427
Zack Williams045b63d2019-01-22 16:30:57 -0700428 def save(
429 self,
430 update_fields=None,
431 always_update_timestamp=False,
432 is_sync_save=False,
433 is_policy_save=False,
434 ):
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700435
Andy Bavier04ee1912019-01-30 14:17:16 -0700436 classname = self._wrapped_class.__class__.__name__
Scott Baker96b995a2017-02-15 16:21:12 -0800437 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700438 log.debug(
439 "save(): is new",
440 classname=classname,
441 syncstep=get_synchronizer_function(),
Zack Williams045b63d2019-01-22 16:30:57 -0700442 )
Zack Williams5c2ea232019-01-30 15:23:01 -0700443 new_class = self.stub.invoke("Create%s" % classname, self._wrapped_class)
Zack Williams045b63d2019-01-22 16:30:57 -0700444 self._wrapped_class = new_class
445 self.is_new = False
Scott Baker96b995a2017-02-15 16:21:12 -0800446 else:
Andy Bavier04ee1912019-01-30 14:17:16 -0700447 if self.has_changed:
Zack Williams5c2ea232019-01-30 15:23:01 -0700448 log.debug(
449 "save(): updated",
450 classname=classname,
451 changed_fields=self.changed_fields,
452 syncstep=get_synchronizer_function(),
453 )
Andy Bavier04ee1912019-01-30 14:17:16 -0700454 else:
Zack Williams5c2ea232019-01-30 15:23:01 -0700455 log.debug(
456 "save(): no changes",
457 classname=classname,
458 syncstep=get_synchronizer_function(),
459 )
Zack Williams045b63d2019-01-22 16:30:57 -0700460 metadata = []
461 if update_fields:
462 metadata.append(("update_fields", ",".join(update_fields)))
463 if always_update_timestamp:
464 metadata.append(("always_update_timestamp", "1"))
465 if is_policy_save:
466 metadata.append(("is_policy_save", "1"))
467 if is_sync_save:
468 metadata.append(("is_sync_save", "1"))
469 self.stub.invoke(
Zack Williams5c2ea232019-01-30 15:23:01 -0700470 "Update%s" % classname, self._wrapped_class, metadata=metadata
Zack Williams045b63d2019-01-22 16:30:57 -0700471 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800472 self.do_post_save_fixups()
Scott Baker96b995a2017-02-15 16:21:12 -0800473
Scott Baker5b7fba02018-10-17 08:46:46 -0700474 # Now that object has saved, reset our initial state for diff calculation
475 self.recompute_initial()
476
Scott Baker96b995a2017-02-15 16:21:12 -0800477 def delete(self):
478 id = self.stub.make_ID(id=self._wrapped_class.id)
479 self.stub.invoke("Delete%s" % self._wrapped_class.__class__.__name__, id)
480
Scott Baker22796cc2017-02-23 16:53:34 -0800481 def tologdict(self):
482 try:
Zack Williams045b63d2019-01-22 16:30:57 -0700483 d = {"model_name": self._wrapped_class.__class__.__name__, "pk": self.pk}
484 except BaseException:
Scott Baker22796cc2017-02-23 16:53:34 -0800485 d = {}
486
487 return d
488
Scott Bakerbb81e152017-03-02 15:28:36 -0800489 @property
Scott Bakerff104cc2017-08-14 15:24:41 -0700490 def leaf_model(self):
491 # Easy case - this model is already the leaf
492 if self.leaf_model_name == self._wrapped_class.__class__.__name__:
493 return self
494
495 # This model is not the leaf, so use the stub to fetch the leaf model
496 return getattr(self.stub, self.leaf_model_name).objects.get(id=self.id)
497
498 @property
Scott Bakerd2543ed2017-03-07 21:46:48 -0800499 def model_name(self):
500 return self._wrapped_class.__class__.__name__
501
502 @property
Scott Bakerbb81e152017-03-02 15:28:36 -0800503 def ansible_tag(self):
504 return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
505
Zack Williams045b63d2019-01-22 16:30:57 -0700506
Scott Bakerb05393b2017-03-01 14:59:55 -0800507class ORMQuerySet(list):
508 """ Makes lists look like django querysets """
Zack Williams045b63d2019-01-22 16:30:57 -0700509
Scott Bakerb05393b2017-03-01 14:59:55 -0800510 def first(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700511 if len(self) > 0:
Scott Bakerb05393b2017-03-01 14:59:55 -0800512 return self[0]
513 else:
514 return None
515
Scott Baker8c7267d2017-03-14 19:34:13 -0700516 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700517 return len(self) > 0
518
Scott Baker8c7267d2017-03-14 19:34:13 -0700519
Scott Baker96b995a2017-02-15 16:21:12 -0800520class ORMLocalObjectManager(object):
521 """ Manages a local list of objects """
522
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800523 def __init__(self, stub, modelName, idList, writeable):
Scott Baker96b995a2017-02-15 16:21:12 -0800524 self._stub = stub
525 self._modelName = modelName
526 self._idList = idList
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800527 self._writeable = writeable
Scott Baker96b995a2017-02-15 16:21:12 -0800528 self._cache = None
529
530 def resolve_queryset(self):
531 if self._cache is not None:
532 return self._cache
533
534 models = []
535 for id in self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700536 models.append(
537 self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id))
538 )
Scott Baker96b995a2017-02-15 16:21:12 -0800539
540 self._cache = models
541
542 return models
543
544 def all(self):
545 models = self.resolve_queryset()
Zack Williams045b63d2019-01-22 16:30:57 -0700546 return [make_ORMWrapper(x, self._stub) for x in models]
Scott Baker96b995a2017-02-15 16:21:12 -0800547
Scott Baker8c7267d2017-03-14 19:34:13 -0700548 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700549 return len(self._idList) > 0
Scott Baker8c7267d2017-03-14 19:34:13 -0700550
Scott Bakera1eae7a2017-06-06 09:20:15 -0700551 def count(self):
552 return len(self._idList)
553
Scott Baker8c7267d2017-03-14 19:34:13 -0700554 def first(self):
555 if self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700556 model = make_ORMWrapper(
557 self._stub.invoke(
558 "Get%s" % self._modelName, self._stub.make_ID(id=self._idList[0])
559 ),
560 self._stub,
561 )
Scott Baker8c7267d2017-03-14 19:34:13 -0700562 return model
563 else:
564 return None
565
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800566 def add(self, model):
567 if not self._writeable:
568 raise Exception("Only ManyToMany lists are writeable")
569
570 if isinstance(model, int):
571 id = model
572 else:
573 if not model.id:
574 raise Exception("Model %s has no id" % model)
575 id = model.id
576
577 self._idList.append(id)
578
579 def remove(self, model):
580 if not self._writeable:
581 raise Exception("Only ManyToMany lists are writeable")
582
583 if isinstance(model, int):
584 id = model
585 else:
586 if not model.id:
587 raise Exception("Model %s has no id" % model)
588 id = model.id
589
590 self._idList.remove(id)
591
Zack Williams045b63d2019-01-22 16:30:57 -0700592
Scott Baker96b995a2017-02-15 16:21:12 -0800593class ORMObjectManager(object):
594 """ Manages a remote list of objects """
595
Scott Bakerac2f2b52017-02-21 14:53:23 -0800596 # constants better agree with common.proto
Scott Bakerea1f4d02018-12-17 10:21:50 -0800597 DEFAULT = 0
598 ALL = 1
Scott Bakerbae9d842017-03-21 10:44:10 -0700599 SYNCHRONIZER_DIRTY_OBJECTS = 2
600 SYNCHRONIZER_DELETED_OBJECTS = 3
601 SYNCHRONIZER_DIRTY_POLICIES = 4
602 SYNCHRONIZER_DELETED_POLICIES = 5
Scott Bakerac2f2b52017-02-21 14:53:23 -0800603
Scott Bakerea1f4d02018-12-17 10:21:50 -0800604 def __init__(self, stub, modelName, packageName, kind=0):
Scott Baker96b995a2017-02-15 16:21:12 -0800605 self._stub = stub
606 self._modelName = modelName
607 self._packageName = packageName
Scott Bakerea1f4d02018-12-17 10:21:50 -0800608 self._kind = kind
Scott Baker96b995a2017-02-15 16:21:12 -0800609
610 def wrap_single(self, obj):
Scott Baker22796cc2017-02-23 16:53:34 -0800611 return make_ORMWrapper(obj, self._stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800612
613 def wrap_list(self, obj):
Zack Williams045b63d2019-01-22 16:30:57 -0700614 result = []
Scott Baker96b995a2017-02-15 16:21:12 -0800615 for item in obj.items:
Scott Baker22796cc2017-02-23 16:53:34 -0800616 result.append(make_ORMWrapper(item, self._stub))
Scott Bakerb05393b2017-03-01 14:59:55 -0800617 return ORMQuerySet(result)
Scott Baker96b995a2017-02-15 16:21:12 -0800618
619 def all(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700620 if self._kind == self.DEFAULT:
621 return self.wrap_list(
622 self._stub.invoke("List%s" % self._modelName, self._stub.make_empty())
623 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800624 else:
625 return self.filter()
Scott Baker96b995a2017-02-15 16:21:12 -0800626
Scott Baker22796cc2017-02-23 16:53:34 -0800627 def first(self):
Scott Bakerea1f4d02018-12-17 10:21:50 -0800628 objs = self.all()
Scott Baker22796cc2017-02-23 16:53:34 -0800629 if not objs:
630 return None
631 return objs[0]
632
Scott Bakerac2f2b52017-02-21 14:53:23 -0800633 def filter(self, **kwargs):
634 q = self._stub.make_Query()
Scott Bakerea1f4d02018-12-17 10:21:50 -0800635 q.kind = self._kind
Scott Bakerac2f2b52017-02-21 14:53:23 -0800636
637 for (name, val) in kwargs.items():
638 el = q.elements.add()
639
640 if name.endswith("__gt"):
641 name = name[:-4]
642 el.operator = el.GREATER_THAN
643 elif name.endswith("__gte"):
644 name = name[:-5]
645 el.operator = el.GREATER_THAN_OR_EQUAL
646 elif name.endswith("__lt"):
647 name = name[:-4]
648 el.operator = el.LESS_THAN
649 elif name.endswith("__lte"):
650 name = name[:-5]
651 el.operator = el.LESS_THAN_OR_EQUAL
Scott Bakere1607b82018-09-20 14:10:59 -0700652 elif name.endswith("__iexact"):
653 name = name[:-8]
654 el.operator = el.IEXACT
Scott Bakerac2f2b52017-02-21 14:53:23 -0800655 else:
656 el.operator = el.EQUAL
657
658 el.name = name
659 if isinstance(val, int):
660 el.iValue = val
661 else:
662 el.sValue = val
663
664 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
665
666 def filter_special(self, kind):
667 q = self._stub.make_Query()
668 q.kind = kind
669 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
670
Scott Baker22796cc2017-02-23 16:53:34 -0800671 def get(self, **kwargs):
Zack Williams5c2ea232019-01-30 15:23:01 -0700672 if list(kwargs.keys()) == ["id"]:
Scott Baker22796cc2017-02-23 16:53:34 -0800673 # the fast and easy case, look it up by id
Zack Williams045b63d2019-01-22 16:30:57 -0700674 return self.wrap_single(
675 self._stub.invoke(
676 "Get%s" % self._modelName, self._stub.make_ID(id=kwargs["id"])
677 )
678 )
Scott Baker22796cc2017-02-23 16:53:34 -0800679 else:
680 # the slightly more difficult case, filter and return the first item
681 objs = self.filter(**kwargs)
682 return objs[0]
Scott Baker96b995a2017-02-15 16:21:12 -0800683
684 def new(self, **kwargs):
Zack Williams045b63d2019-01-22 16:30:57 -0700685 if self._kind != ORMObjectManager.DEFAULT:
686 raise Exception(
687 "Creating objects is only supported by the DEFAULT object manager"
688 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800689
Scott Bakeraa556b02017-03-07 16:07:34 -0800690 cls = self._stub.all_grpc_classes[self._modelName]
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700691 o = make_ORMWrapper(cls(), self._stub, is_new=True)
Zack Williams045b63d2019-01-22 16:30:57 -0700692 for (k, v) in kwargs.items():
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700693 setattr(o, k, v)
Scott Baker5b7fba02018-10-17 08:46:46 -0700694 o.recompute_initial()
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700695 return o
Scott Baker96b995a2017-02-15 16:21:12 -0800696
Zack Williams045b63d2019-01-22 16:30:57 -0700697
Scott Baker96b995a2017-02-15 16:21:12 -0800698class ORMModelClass(object):
699 def __init__(self, stub, model_name, package_name):
Scott Baker22796cc2017-02-23 16:53:34 -0800700 self.model_name = model_name
Scott Bakeraa556b02017-03-07 16:07:34 -0800701 self._stub = stub
Scott Baker96b995a2017-02-15 16:21:12 -0800702 self.objects = ORMObjectManager(stub, model_name, package_name)
Zack Williams045b63d2019-01-22 16:30:57 -0700703 self.deleted_objects = ORMObjectManager(
704 stub,
705 model_name,
706 package_name,
707 ORMObjectManager.SYNCHRONIZER_DELETED_OBJECTS,
708 )
Scott Baker96b995a2017-02-15 16:21:12 -0800709
Scott Bakerbb81e152017-03-02 15:28:36 -0800710 @property
Scott Baker22796cc2017-02-23 16:53:34 -0800711 def __name__(self):
712 return self.model_name
713
Scott Bakeraa556b02017-03-07 16:07:34 -0800714 @property
715 def content_type_id(self):
716 return self._stub.reverse_content_type_map[self.model_name]
717
Scott Baker8a6d91f2017-03-22 11:23:11 -0700718 def __call__(self, *args, **kwargs):
719 return self.objects.new(*args, **kwargs)
720
Zack Williams045b63d2019-01-22 16:30:57 -0700721
Scott Baker96b995a2017-02-15 16:21:12 -0800722class ORMStub(object):
Zack Williams045b63d2019-01-22 16:30:57 -0700723 def __init__(
724 self,
725 stub,
726 protos,
727 package_name,
728 invoker=None,
729 caller_kind="grpcapi",
730 empty=None,
731 enable_backoff=True,
732 restart_on_disconnect=False,
733 ):
Scott Baker96b995a2017-02-15 16:21:12 -0800734 self.grpc_stub = stub
Scott Bakerb96ba432018-02-26 09:53:48 -0800735 self.protos = protos
736 self.common_protos = protos.common__pb2
Scott Baker96b995a2017-02-15 16:21:12 -0800737 self.all_model_names = []
Scott Bakeraa556b02017-03-07 16:07:34 -0800738 self.all_grpc_classes = {}
739 self.content_type_map = {}
740 self.reverse_content_type_map = {}
Scott Bakeref8d85d2017-02-21 16:44:28 -0800741 self.invoker = invoker
Scott Baker22796cc2017-02-23 16:53:34 -0800742 self.caller_kind = caller_kind
Scott Baker500f8c72017-05-19 09:41:50 -0700743 self.enable_backoff = enable_backoff
Scott Bakerb06e3e02017-12-12 11:05:53 -0800744 self.restart_on_disconnect = restart_on_disconnect
Scott Baker96b995a2017-02-15 16:21:12 -0800745
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700746 if not empty:
Scott Bakerb96ba432018-02-26 09:53:48 -0800747 empty = self.protos.google_dot_protobuf_dot_empty__pb2.Empty
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700748 self._empty = empty
749
Scott Baker96b995a2017-02-15 16:21:12 -0800750 for name in dir(stub):
Zack Williams045b63d2019-01-22 16:30:57 -0700751 if name.startswith("Get"):
752 model_name = name[3:]
753 setattr(self, model_name, ORMModelClass(self, model_name, package_name))
Scott Baker96b995a2017-02-15 16:21:12 -0800754
Zack Williams045b63d2019-01-22 16:30:57 -0700755 self.all_model_names.append(model_name)
Scott Baker96b995a2017-02-15 16:21:12 -0800756
Zack Williams045b63d2019-01-22 16:30:57 -0700757 grpc_class = getattr(self.protos, model_name)
758 self.all_grpc_classes[model_name] = grpc_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800759
Zack Williams045b63d2019-01-22 16:30:57 -0700760 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions._FindExtensionByName(
761 "xos.contentTypeId"
762 )
763 if ct:
764 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions[ct]
765 if ct:
766 self.content_type_map[ct] = model_name
767 self.reverse_content_type_map[model_name] = ct
Scott Bakeraa556b02017-03-07 16:07:34 -0800768
769 def genericForeignKeyResolve(self, content_type_id, id):
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700770 if content_type_id.endswith("_decl"):
771 content_type_id = content_type_id[:-5]
772
773 if content_type_id not in self.content_type_map:
Zack Williams045b63d2019-01-22 16:30:57 -0700774 raise ORMGenericContentNotFoundException(
775 "Content_type %s not found in self.content_type_map" % content_type_id
776 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700777
Scott Bakeraa556b02017-03-07 16:07:34 -0800778 model_name = self.content_type_map[content_type_id]
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700779
Scott Bakeraa556b02017-03-07 16:07:34 -0800780 model = getattr(self, model_name)
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700781 objs = model.objects.filter(id=id)
782 if not objs:
Zack Williams045b63d2019-01-22 16:30:57 -0700783 raise ORMGenericObjectNotFoundException(
784 "Object %s of model %s was not found" % (id, model_name)
785 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700786
Scott Bakeraa556b02017-03-07 16:07:34 -0800787 return model.objects.get(id=id)
788
Scott Baker22796cc2017-02-23 16:53:34 -0800789 def add_default_metadata(self, metadata):
Zack Williams045b63d2019-01-22 16:30:57 -0700790 default_metadata = [("caller_kind", self.caller_kind)]
Scott Baker22796cc2017-02-23 16:53:34 -0800791
Scott Bakerd8246712018-07-12 18:08:31 -0700792 # introspect to see if we're running from a synchronizer thread
793 if getattr(threading.current_thread(), "is_sync_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700794 default_metadata.append(("is_sync_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700795
796 # introspect to see if we're running from a model_policy thread
797 if getattr(threading.current_thread(), "is_policy_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700798 default_metadata.append(("is_policy_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700799
Scott Baker22796cc2017-02-23 16:53:34 -0800800 # build up a list of metadata keys we already have
Zack Williams045b63d2019-01-22 16:30:57 -0700801 md_keys = [x[0] for x in metadata]
Scott Baker22796cc2017-02-23 16:53:34 -0800802
803 # add any defaults that we don't already have
804 for md in default_metadata:
805 if md[0] not in md_keys:
Zack Williams045b63d2019-01-22 16:30:57 -0700806 metadata.append((md[0], md[1]))
Scott Baker22796cc2017-02-23 16:53:34 -0800807
Scott Baker57c74822017-02-23 11:13:04 -0800808 def invoke(self, name, request, metadata=[]):
Scott Baker22796cc2017-02-23 16:53:34 -0800809 self.add_default_metadata(metadata)
810
Scott Bakeref8d85d2017-02-21 16:44:28 -0800811 if self.invoker:
812 # Hook in place to call Chameleon's invoke method, as soon as we
813 # have rewritten the synchronizer to use reactor.
Zack Williams045b63d2019-01-22 16:30:57 -0700814 return self.invoker.invoke(
815 self.grpc_stub.__class__, name, request, metadata={}
816 ).result[0]
Scott Baker500f8c72017-05-19 09:41:50 -0700817 elif self.enable_backoff:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800818 # Our own retry mechanism. This works fine if there is a temporary
819 # failure in connectivity, but does not re-download gRPC schema.
Scott Baker500f8c72017-05-19 09:41:50 -0700820 import grpc
Zack Williams045b63d2019-01-22 16:30:57 -0700821
Scott Bakerb06e3e02017-12-12 11:05:53 -0800822 backoff = [0.5, 1, 2, 4, 8]
Scott Bakeref8d85d2017-02-21 16:44:28 -0800823 while True:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800824 try:
825 method = getattr(self.grpc_stub, name)
Scott Baker57c74822017-02-23 11:13:04 -0800826 return method(request, metadata=metadata)
Zack Williams045b63d2019-01-22 16:30:57 -0700827 except grpc._channel._Rendezvous as e:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800828 code = e.code()
829 if code == grpc.StatusCode.UNAVAILABLE:
Scott Bakerb06e3e02017-12-12 11:05:53 -0800830 if self.restart_on_disconnect:
831 # This is a blunt technique... We lost connectivity to the core, and we don't know that
832 # the core is still serving up the same models it was when we established connectivity,
833 # so restart the synchronizer.
834 # TODO: Hash check on the core models to tell if something changed would be better.
Zack Williams045b63d2019-01-22 16:30:57 -0700835 os.execv(sys.executable, ["python"] + sys.argv)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800836 if not backoff:
837 raise Exception("No more retries on %s" % name)
838 time.sleep(backoff.pop(0))
839 else:
840 raise
Zack Williams045b63d2019-01-22 16:30:57 -0700841 except BaseException:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800842 raise
Scott Baker500f8c72017-05-19 09:41:50 -0700843 else:
844 method = getattr(self.grpc_stub, name)
845 return method(request, metadata=metadata)
846
Scott Baker96b995a2017-02-15 16:21:12 -0800847 def make_ID(self, id):
Scott Bakerb96ba432018-02-26 09:53:48 -0800848 return getattr(self.common_protos, "ID")(id=id)
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700849
850 def make_empty(self):
851 return self._empty()
Scott Baker96b995a2017-02-15 16:21:12 -0800852
Scott Bakerac2f2b52017-02-21 14:53:23 -0800853 def make_Query(self):
Scott Bakerb96ba432018-02-26 09:53:48 -0800854 return getattr(self.common_protos, "Query")()
Scott Bakerac2f2b52017-02-21 14:53:23 -0800855
Scott Bakerf6145a22017-03-29 14:50:25 -0700856 def listObjects(self):
857 return self.all_model_names
858
Zack Williams045b63d2019-01-22 16:30:57 -0700859
Scott Baker22796cc2017-02-23 16:53:34 -0800860def register_convenience_wrapper(class_name, wrapper):
861 global convenience_wrappers
Scott Baker96b995a2017-02-15 16:21:12 -0800862
Scott Baker22796cc2017-02-23 16:53:34 -0800863 convenience_wrappers[class_name] = wrapper
864
Zack Williams045b63d2019-01-22 16:30:57 -0700865
Scott Baker22796cc2017-02-23 16:53:34 -0800866def make_ORMWrapper(wrapped_class, *args, **kwargs):
Scott Baker2f314d52018-08-24 08:31:19 -0700867 cls = None
868
869 if (not cls) and wrapped_class.__class__.__name__ in convenience_wrappers:
Scott Baker22796cc2017-02-23 16:53:34 -0800870 cls = convenience_wrappers[wrapped_class.__class__.__name__]
Scott Baker2f314d52018-08-24 08:31:19 -0700871
Zack Williams045b63d2019-01-22 16:30:57 -0700872 if not cls:
Scott Baker2f314d52018-08-24 08:31:19 -0700873 # Search the list of class names for this model to see if we have any applicable wrappers. The list is always
874 # sorted from most specific to least specific, so the first one we find will automatically be the most relevant
875 # one. If we don't find any, then default to ORMWrapper
876
877 # Note: Only works on objects that have been fetched from the server, not objects that are created on the
878 # client. This is because wrapped_class.class_names is filled in by the server.
879
880 # TODO(smbaker): Ought to be able to make this work with newly created objects after they are saved.
881
882 for name in wrapped_class.class_names.split(","):
883 if name in convenience_wrappers:
884 cls = convenience_wrappers[name]
885
Zack Williams045b63d2019-01-22 16:30:57 -0700886 if not cls:
Scott Baker22796cc2017-02-23 16:53:34 -0800887 cls = ORMWrapper
888
889 return cls(wrapped_class, *args, **kwargs)
890
Zack Williams045b63d2019-01-22 16:30:57 -0700891
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200892def import_convenience_methods():
Scott Baker8203dc62019-03-14 16:16:19 -0700893 # The ORM has several built-in convenience methods that are contained here
894 lib_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
895 base_convenience_dir = os.path.join(lib_dir, "convenience")
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200896
Scott Baker8203dc62019-03-14 16:16:19 -0700897 # Service convenience methods are placed here during dynamicload
898 service_convenience_dir = "/var/run/xosapi/convenience"
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200899
Scott Baker8203dc62019-03-14 16:16:19 -0700900 for api_convenience_dir in [base_convenience_dir, service_convenience_dir]:
901 log.info("Loading convenience methods", api_convenience_dir=api_convenience_dir)
902
903 if not os.path.exists(api_convenience_dir):
904 log.info("No convenience methods found", api_convenience_dir=api_convenience_dir)
905 continue
906
907 for file in os.listdir(api_convenience_dir):
908 if file.endswith(".py") and "test" not in file:
909 pathname = os.path.join(api_convenience_dir, file)
910 try:
911 log.debug("Loading: %s" % file)
912 imp.load_source(file[:-3], pathname)
913 except Exception:
914 log.exception(
915 "Cannot import api convenience method for: %s, %s"
916 % (file[:-3], pathname)
917 )