blob: a937e18d16841fc0a7fd9af2f88fc7d072b2a74c [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 """
Zack Williams5c2ea232019-01-30 15:23:01 -070057 for file, line, func, stmt in traceback.extract_stack():
Andy Bavier04ee1912019-01-30 14:17:16 -070058 if file.startswith("/opt/xos/synchronizers"):
Andy Bavierfe87b662019-06-10 13:51:19 -070059 result = "%s:%s()" % (file, func)
60 return result
61 return None
Zack Williams045b63d2019-01-22 16:30:57 -070062
Zack Williams5c2ea232019-01-30 15:23:01 -070063
Scott Bakerd0f1dc12018-04-23 12:05:32 -070064class ORMGenericContentNotFoundException(Exception):
65 pass
66
Zack Williams045b63d2019-01-22 16:30:57 -070067
Scott Bakerd0f1dc12018-04-23 12:05:32 -070068class ORMGenericObjectNotFoundException(Exception):
69 pass
70
Zack Williams045b63d2019-01-22 16:30:57 -070071
Scott Baker96b995a2017-02-15 16:21:12 -080072class ORMWrapper(object):
73 """ Wraps a protobuf object to provide ORM features """
74
75 def __init__(self, wrapped_class, stub, is_new=False):
76 super(ORMWrapper, self).__setattr__("_wrapped_class", wrapped_class)
77 super(ORMWrapper, self).__setattr__("stub", stub)
78 super(ORMWrapper, self).__setattr__("cache", {})
79 super(ORMWrapper, self).__setattr__("reverse_cache", {})
Sapan Bhatia71f57682017-08-23 20:09:08 -040080 super(ORMWrapper, self).__setattr__("synchronizer_step", None)
Sapan Bhatia2b307f72017-11-02 11:39:17 -040081 super(ORMWrapper, self).__setattr__("dependent", None)
Scott Baker96b995a2017-02-15 16:21:12 -080082 super(ORMWrapper, self).__setattr__("is_new", is_new)
Scott Bakerc4156c32017-12-08 10:58:21 -080083 super(ORMWrapper, self).__setattr__("post_save_fixups", [])
Zack Williams045b63d2019-01-22 16:30:57 -070084 fkmap = self.gen_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080085 super(ORMWrapper, self).__setattr__("_fkmap", fkmap)
Zack Williams045b63d2019-01-22 16:30:57 -070086 reverse_fkmap = self.gen_reverse_fkmap()
Scott Baker96b995a2017-02-15 16:21:12 -080087 super(ORMWrapper, self).__setattr__("_reverse_fkmap", reverse_fkmap)
Scott Baker5b7fba02018-10-17 08:46:46 -070088 super(ORMWrapper, self).__setattr__("_initial", self._dict)
89
Zack Williams045b63d2019-01-22 16:30:57 -070090 def fields_differ(self, f1, f2):
91 return f1 != f2
Scott Baker5b7fba02018-10-17 08:46:46 -070092
93 @property
94 def _dict(self):
95 """ Return a dictionary of {fieldname: fieldvalue} for the object.
96
97 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
98 that are set to default values. ORM ignores fields that are set to default values.
99 """
Zack Williams045b63d2019-01-22 16:30:57 -0700100 d = {}
Scott Baker5b7fba02018-10-17 08:46:46 -0700101 for (fieldDesc, val) in self._wrapped_class.ListFields():
102 name = fieldDesc.name
103 d[name] = val
104 return d
105
106 @property
107 def diff(self):
108 d1 = self._initial
109 d2 = self._dict
Zack Williams5c2ea232019-01-30 15:23:01 -0700110 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Zack Williams045b63d2019-01-22 16:30:57 -0700111 diffs = []
Scott Baker5b7fba02018-10-17 08:46:46 -0700112 for k in all_field_names:
Zack Williams045b63d2019-01-22 16:30:57 -0700113 if d1.get(k, None) != d2.get(k, None):
114 diffs.append((k, (d1.get(k, None), d2.get(k, None))))
Scott Baker5b7fba02018-10-17 08:46:46 -0700115
Zack Williams045b63d2019-01-22 16:30:57 -0700116 # 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 -0700117 return dict(diffs)
118
119 @property
120 def has_changed(self):
121 return bool(self.diff)
122
123 @property
124 def changed_fields(self):
125 """ Return the list of changed fields.
126
127 This differs for the xos-core implementation of XOSBase. For new object, XOSBase will include field names
128 that are set to default values.
129 """
130 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700131 return list(self._dict.keys())
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700132
Zack Williams5c2ea232019-01-30 15:23:01 -0700133 return list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700134
135 def has_field_changed(self, field_name):
Zack Williams5c2ea232019-01-30 15:23:01 -0700136 return field_name in list(self.diff.keys())
Scott Baker5b7fba02018-10-17 08:46:46 -0700137
138 def get_field_diff(self, field_name):
139 return self.diff.get(field_name, None)
140
141 def recompute_initial(self):
142 self._initial = self._dict
143
144 def save_changed_fields(self, always_update_timestamp=False):
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700145
146 # we need to ignore many-to-many fields as they are saved by do_post_save_fixups
147 # and can't be sent over the wire
148 m2m_fields = [v['src_fieldName'] for k, v in self._reverse_fkmap.items()]
149
Scott Baker5b7fba02018-10-17 08:46:46 -0700150 if self.has_changed:
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700151 update_fields = [f for f in self.changed_fields if f not in m2m_fields]
Scott Baker5b7fba02018-10-17 08:46:46 -0700152 if always_update_timestamp and "updated" not in update_fields:
153 update_fields.append("updated")
Zack Williams045b63d2019-01-22 16:30:57 -0700154 self.save(
155 update_fields=sorted(update_fields),
156 always_update_timestamp=always_update_timestamp,
157 )
Scott Baker96b995a2017-02-15 16:21:12 -0800158
Scott Bakerd78f6472017-03-14 17:30:14 -0700159 def create_attr(self, name, value=None):
160 """ setattr(self, ...) will fail for attributes that don't exist in the
161 wrapped grpc class. This is by design. However, if someone really
162 does want to attach a new attribute to this class, then they can
163 call create_attr()
164 """
165 super(ORMWrapper, self).__setattr__(name, value)
166
Scott Baker03a163f2017-05-17 09:21:47 -0700167 def get_generic_foreignkeys(self):
168 """ this is a placeholder until generic foreign key support is added
169 to xproto.
170 """
171 return []
172
Scott Baker96b995a2017-02-15 16:21:12 -0800173 def gen_fkmap(self):
174 fkmap = {}
175
Zack Williams5c2ea232019-01-30 15:23:01 -0700176 all_field_names = list(self._wrapped_class.DESCRIPTOR.fields_by_name.keys())
Scott Bakeraa556b02017-03-07 16:07:34 -0800177
Scott Baker96b995a2017-02-15 16:21:12 -0800178 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700179 if name.endswith("_id"):
180 foreignKey = field.GetOptions().Extensions._FindExtensionByName(
181 "xos.foreignKey"
182 )
183 fk = field.GetOptions().Extensions[foreignKey]
184 if fk and fk.modelName:
185 fkdict = {
186 "src_fieldName": name,
187 "modelName": fk.modelName,
188 "kind": "fk",
189 }
190 if fk.reverseFieldName:
191 fkdict["reverse_fieldName"] = fk.reverseFieldName
192 fkmap[name[:-3]] = fkdict
193 else:
194 # If there's a corresponding _type_id field, then see if this
195 # is a generic foreign key.
196 type_name = name[:-3] + "_type_id"
197 if type_name in all_field_names:
198 fkmap[name[:-3]] = {
199 "src_fieldName": name,
200 "ct_fieldName": type_name,
201 "kind": "generic_fk",
202 }
Scott Baker96b995a2017-02-15 16:21:12 -0800203
Scott Baker03a163f2017-05-17 09:21:47 -0700204 for gfk in self.get_generic_foreignkeys():
Zack Williams045b63d2019-01-22 16:30:57 -0700205 fkmap[gfk["name"]] = {
206 "src_fieldName": gfk["id"],
207 "ct_fieldName": gfk["content_type"],
208 "kind": "generic_fk",
209 }
Scott Baker03a163f2017-05-17 09:21:47 -0700210
Scott Baker96b995a2017-02-15 16:21:12 -0800211 return fkmap
212
213 def gen_reverse_fkmap(self):
214 reverse_fkmap = {}
215
216 for (name, field) in self._wrapped_class.DESCRIPTOR.fields_by_name.items():
Zack Williams045b63d2019-01-22 16:30:57 -0700217 if name.endswith("_ids"):
218 reverseForeignKey = field.GetOptions().Extensions._FindExtensionByName(
219 "xos.reverseForeignKey"
220 )
221 fk = field.GetOptions().Extensions[reverseForeignKey]
222 if fk and fk.modelName:
223 reverse_fkmap[name[:-4]] = {
224 "src_fieldName": name,
225 "modelName": fk.modelName,
226 "writeable": False,
227 }
228 else:
229 manyToManyForeignKey = field.GetOptions().Extensions._FindExtensionByName(
230 "xos.manyToManyForeignKey"
231 )
232 fk = field.GetOptions().Extensions[manyToManyForeignKey]
233 if fk and fk.modelName:
234 reverse_fkmap[name[:-4]] = {
235 "src_fieldName": name,
236 "modelName": fk.modelName,
237 "writeable": True,
238 }
Scott Baker96b995a2017-02-15 16:21:12 -0800239
240 return reverse_fkmap
241
242 def fk_resolve(self, name):
243 if name in self.cache:
Scott Bakerc4156c32017-12-08 10:58:21 -0800244 return self.cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800245
246 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800247 fk_kind = fk_entry["kind"]
248 fk_id = getattr(self, fk_entry["src_fieldName"])
249
250 if not fk_id:
251 return None
252
Zack Williams045b63d2019-01-22 16:30:57 -0700253 if fk_kind == "fk":
254 id = self.stub.make_ID(id=fk_id)
Scott Bakeraa556b02017-03-07 16:07:34 -0800255 dest_model = self.stub.invoke("Get%s" % fk_entry["modelName"], id)
256
Zack Williams045b63d2019-01-22 16:30:57 -0700257 elif fk_kind == "generic_fk":
258 dest_model = self.stub.genericForeignKeyResolve(
259 getattr(self, fk_entry["ct_fieldName"]), fk_id
260 )._wrapped_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800261
262 else:
263 raise Exception("unknown fk_kind")
Scott Baker96b995a2017-02-15 16:21:12 -0800264
Scott Bakerc4156c32017-12-08 10:58:21 -0800265 dest_model = make_ORMWrapper(dest_model, self.stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800266 self.cache[name] = dest_model
267
Scott Bakerc4156c32017-12-08 10:58:21 -0800268 return dest_model
Scott Baker96b995a2017-02-15 16:21:12 -0800269
270 def reverse_fk_resolve(self, name):
271 if name not in self.reverse_cache:
272 fk_entry = self._reverse_fkmap[name]
Zack Williams045b63d2019-01-22 16:30:57 -0700273 self.reverse_cache[name] = ORMLocalObjectManager(
274 self.stub,
275 fk_entry["modelName"],
276 getattr(self, fk_entry["src_fieldName"]),
277 fk_entry["writeable"],
278 )
Scott Baker96b995a2017-02-15 16:21:12 -0800279
Scott Baker7ab456b2019-01-08 14:58:13 -0800280 return self.reverse_cache[name]
Scott Baker96b995a2017-02-15 16:21:12 -0800281
Scott Bakere72e7612017-02-20 10:07:09 -0800282 def fk_set(self, name, model):
283 fk_entry = self._fkmap[name]
Scott Bakeraa556b02017-03-07 16:07:34 -0800284 fk_kind = fk_entry["kind"]
Scott Bakera1eae7a2017-06-06 09:20:15 -0700285 if model:
286 id = model.id
287 else:
288 id = 0
Scott Bakere72e7612017-02-20 10:07:09 -0800289 setattr(self._wrapped_class, fk_entry["src_fieldName"], id)
290
Zack Williams045b63d2019-01-22 16:30:57 -0700291 if fk_kind == "generic_fk":
292 setattr(
293 self._wrapped_class,
294 fk_entry["ct_fieldName"],
295 model.self_content_type_id,
296 )
Scott Bakeraa556b02017-03-07 16:07:34 -0800297
Scott Bakerc4156c32017-12-08 10:58:21 -0800298 if name in self.cache:
299 old_model = self.cache[name]
300 if fk_entry.get("reverse_fieldName"):
301 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700302 self.post_save_fixups.append(
303 {
304 "src_fieldName": fk_entry["src_fieldName"],
305 "dest_id": id,
306 "dest_model": old_model,
307 "remove": True,
308 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
309 }
310 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800311 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800312
Scott Bakerc4156c32017-12-08 10:58:21 -0800313 if model:
314 self.cache[name] = model
315 if fk_entry.get("reverse_fieldName"):
316 # Note this fk change so that we can update the destination model after we save.
Zack Williams045b63d2019-01-22 16:30:57 -0700317 self.post_save_fixups.append(
318 {
319 "src_fieldName": fk_entry["src_fieldName"],
320 "dest_id": id,
321 "dest_model": model,
322 "remove": False,
323 "reverse_fieldName": fk_entry.get("reverse_fieldName"),
324 }
325 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800326 elif name in self.cache:
327 del self.cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800328
Scott Bakerc4156c32017-12-08 10:58:21 -0800329 def do_post_save_fixups(self):
330 # Perform post-save foreign key fixups.
331 # Fixup the models that we've set a foreign key to so that their in-memory representation has the correct
332 # reverse foreign key back to us. We can only do this after a save, because self.id isn't known until
333 # after save.
334 # See unit test test_foreign_key_set_without_invalidate
335 for fixup in self.post_save_fixups:
336 model = fixup["dest_model"]
337 reverse_fieldName_ids = fixup["reverse_fieldName"] + "_ids"
338 if not hasattr(model, reverse_fieldName_ids):
339 continue
340 if fixup["remove"]:
341 reverse_ids = getattr(model, reverse_fieldName_ids)
342 if self.id in reverse_ids:
343 reverse_ids.remove(self.id)
344 else:
345 reverse_ids = getattr(model, reverse_fieldName_ids)
346 if self.id not in reverse_ids:
347 reverse_ids.append(self.id)
348 model.invalidate_cache(fixup["reverse_fieldName"])
349 self.post_save_fixups = []
Scott Bakere72e7612017-02-20 10:07:09 -0800350
Scott Baker96b995a2017-02-15 16:21:12 -0800351 def __getattr__(self, name, *args, **kwargs):
352 # note: getattr is only called for attributes that do not exist in
353 # self.__dict__
354
Scott Baker186372f2017-02-23 13:49:36 -0800355 # pk is a synonym for id
Zack Williams045b63d2019-01-22 16:30:57 -0700356 if name == "pk":
Scott Baker186372f2017-02-23 13:49:36 -0800357 name = "id"
358
Zack Williams5c2ea232019-01-30 15:23:01 -0700359 if name in list(self._fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800360 return self.fk_resolve(name)
361
Zack Williams5c2ea232019-01-30 15:23:01 -0700362 if name in list(self._reverse_fkmap.keys()):
Scott Baker96b995a2017-02-15 16:21:12 -0800363 return self.reverse_fk_resolve(name)
364
Scott Baker37cf9e22018-08-20 14:39:33 -0700365 try:
366 # When sending a reply, XOS will leave the field unset if it is None in the data model. If
367 # HasField(<fieldname>)==False for an existing object, then the caller can infer that field was set to
368 # None.
369 if (not self.is_new) and (not self._wrapped_class.HasField(name)):
370 return None
371 except ValueError:
372 # ValueError is thrown if the field does not exist. We will handle that case in the getattr() below.
373 pass
374
Scott Baker96b995a2017-02-15 16:21:12 -0800375 return getattr(self._wrapped_class, name, *args, **kwargs)
376
377 def __setattr__(self, name, value):
Zack Williams5c2ea232019-01-30 15:23:01 -0700378 if name in list(self._fkmap.keys()):
Scott Bakere72e7612017-02-20 10:07:09 -0800379 self.fk_set(name, value)
380 elif name in self.__dict__:
Zack Williams045b63d2019-01-22 16:30:57 -0700381 super(ORMWrapper, self).__setattr__(name, value)
Scott Baker37cf9e22018-08-20 14:39:33 -0700382 elif value is None:
383 # When handling requests, XOS interprets gRPC HasField(<fieldname>)==False to indicate that the caller
384 # has not set the field and wants it to continue to use its existing (or default) value. That leaves us
385 # with no easy way to support setting a field to None.
386 raise ValueError("Setting a non-foreignkey field to None is not supported")
Scott Baker96b995a2017-02-15 16:21:12 -0800387 else:
388 setattr(self._wrapped_class, name, value)
389
390 def __repr__(self):
Scott Bakerd1940972017-05-01 15:45:32 -0700391 class_name = self._wrapped_class.__class__.__name__
392 id = getattr(self._wrapped_class, "id", "noid")
393 name = getattr(self._wrapped_class, "name", None)
394 if name:
395 return "<%s: %s>" % (class_name, name)
396 else:
397 return "<%s: id-%s>" % (class_name, id)
398
399 def __str__(self):
400 class_name = self._wrapped_class.__class__.__name__
401 id = getattr(self._wrapped_class, "id", "noid")
402 name = getattr(self._wrapped_class, "name", None)
403 if name:
404 return name
405 else:
406 return "%s-%s" % (class_name, id)
407
408 def dumpstr(self):
Scott Baker96b995a2017-02-15 16:21:12 -0800409 return self._wrapped_class.__repr__()
410
Scott Bakerd1940972017-05-01 15:45:32 -0700411 def dump(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700412 print(self.dumpstr())
Scott Bakerd1940972017-05-01 15:45:32 -0700413
Scott Bakere72e7612017-02-20 10:07:09 -0800414 def invalidate_cache(self, name=None):
415 if name:
416 if name in self.cache:
417 del self.cache[name]
418 if name in self.reverse_cache:
419 del self.reverse_cache[name]
Scott Bakere72e7612017-02-20 10:07:09 -0800420 else:
421 self.cache.clear()
422 self.reverse_cache.clear()
Scott Bakere72e7612017-02-20 10:07:09 -0800423
Zack Williams045b63d2019-01-22 16:30:57 -0700424 def save(
425 self,
426 update_fields=None,
427 always_update_timestamp=False,
428 is_sync_save=False,
429 is_policy_save=False,
430 ):
Matteo Scandolo2fd7b872019-06-06 10:05:42 -0700431
Andy Bavier04ee1912019-01-30 14:17:16 -0700432 classname = self._wrapped_class.__class__.__name__
Scott Baker96b995a2017-02-15 16:21:12 -0800433 if self.is_new:
Zack Williams5c2ea232019-01-30 15:23:01 -0700434 log.debug(
435 "save(): is new",
436 classname=classname,
437 syncstep=get_synchronizer_function(),
Zack Williams045b63d2019-01-22 16:30:57 -0700438 )
Zack Williams5c2ea232019-01-30 15:23:01 -0700439 new_class = self.stub.invoke("Create%s" % classname, self._wrapped_class)
Zack Williams045b63d2019-01-22 16:30:57 -0700440 self._wrapped_class = new_class
441 self.is_new = False
Scott Baker96b995a2017-02-15 16:21:12 -0800442 else:
Andy Bavier04ee1912019-01-30 14:17:16 -0700443 if self.has_changed:
Zack Williams5c2ea232019-01-30 15:23:01 -0700444 log.debug(
445 "save(): updated",
446 classname=classname,
447 changed_fields=self.changed_fields,
448 syncstep=get_synchronizer_function(),
449 )
Andy Bavier04ee1912019-01-30 14:17:16 -0700450 else:
Zack Williams5c2ea232019-01-30 15:23:01 -0700451 log.debug(
452 "save(): no changes",
453 classname=classname,
454 syncstep=get_synchronizer_function(),
455 )
Zack Williams045b63d2019-01-22 16:30:57 -0700456 metadata = []
457 if update_fields:
458 metadata.append(("update_fields", ",".join(update_fields)))
459 if always_update_timestamp:
460 metadata.append(("always_update_timestamp", "1"))
461 if is_policy_save:
462 metadata.append(("is_policy_save", "1"))
463 if is_sync_save:
464 metadata.append(("is_sync_save", "1"))
465 self.stub.invoke(
Zack Williams5c2ea232019-01-30 15:23:01 -0700466 "Update%s" % classname, self._wrapped_class, metadata=metadata
Zack Williams045b63d2019-01-22 16:30:57 -0700467 )
Scott Bakerc4156c32017-12-08 10:58:21 -0800468 self.do_post_save_fixups()
Scott Baker96b995a2017-02-15 16:21:12 -0800469
Scott Baker5b7fba02018-10-17 08:46:46 -0700470 # Now that object has saved, reset our initial state for diff calculation
471 self.recompute_initial()
472
Scott Baker96b995a2017-02-15 16:21:12 -0800473 def delete(self):
474 id = self.stub.make_ID(id=self._wrapped_class.id)
475 self.stub.invoke("Delete%s" % self._wrapped_class.__class__.__name__, id)
476
Scott Baker22796cc2017-02-23 16:53:34 -0800477 def tologdict(self):
478 try:
Zack Williams045b63d2019-01-22 16:30:57 -0700479 d = {"model_name": self._wrapped_class.__class__.__name__, "pk": self.pk}
480 except BaseException:
Scott Baker22796cc2017-02-23 16:53:34 -0800481 d = {}
482
483 return d
484
Scott Bakerbb81e152017-03-02 15:28:36 -0800485 @property
Scott Bakerff104cc2017-08-14 15:24:41 -0700486 def leaf_model(self):
487 # Easy case - this model is already the leaf
488 if self.leaf_model_name == self._wrapped_class.__class__.__name__:
489 return self
490
491 # This model is not the leaf, so use the stub to fetch the leaf model
492 return getattr(self.stub, self.leaf_model_name).objects.get(id=self.id)
493
494 @property
Scott Bakerd2543ed2017-03-07 21:46:48 -0800495 def model_name(self):
496 return self._wrapped_class.__class__.__name__
497
498 @property
Scott Bakerbb81e152017-03-02 15:28:36 -0800499 def ansible_tag(self):
500 return "%s_%s" % (self._wrapped_class.__class__.__name__, self.id)
501
Zack Williams045b63d2019-01-22 16:30:57 -0700502
Scott Bakerb05393b2017-03-01 14:59:55 -0800503class ORMQuerySet(list):
504 """ Makes lists look like django querysets """
Zack Williams045b63d2019-01-22 16:30:57 -0700505
Scott Bakerb05393b2017-03-01 14:59:55 -0800506 def first(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700507 if len(self) > 0:
Scott Bakerb05393b2017-03-01 14:59:55 -0800508 return self[0]
509 else:
510 return None
511
Scott Baker8c7267d2017-03-14 19:34:13 -0700512 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700513 return len(self) > 0
514
Scott Baker8c7267d2017-03-14 19:34:13 -0700515
Scott Baker96b995a2017-02-15 16:21:12 -0800516class ORMLocalObjectManager(object):
517 """ Manages a local list of objects """
518
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800519 def __init__(self, stub, modelName, idList, writeable):
Scott Baker96b995a2017-02-15 16:21:12 -0800520 self._stub = stub
521 self._modelName = modelName
522 self._idList = idList
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800523 self._writeable = writeable
Scott Baker96b995a2017-02-15 16:21:12 -0800524 self._cache = None
525
526 def resolve_queryset(self):
527 if self._cache is not None:
528 return self._cache
529
530 models = []
531 for id in self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700532 models.append(
533 self._stub.invoke("Get%s" % self._modelName, self._stub.make_ID(id=id))
534 )
Scott Baker96b995a2017-02-15 16:21:12 -0800535
536 self._cache = models
537
538 return models
539
540 def all(self):
541 models = self.resolve_queryset()
Zack Williams045b63d2019-01-22 16:30:57 -0700542 return [make_ORMWrapper(x, self._stub) for x in models]
Scott Baker96b995a2017-02-15 16:21:12 -0800543
Scott Baker8c7267d2017-03-14 19:34:13 -0700544 def exists(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700545 return len(self._idList) > 0
Scott Baker8c7267d2017-03-14 19:34:13 -0700546
Scott Bakera1eae7a2017-06-06 09:20:15 -0700547 def count(self):
548 return len(self._idList)
549
Scott Baker8c7267d2017-03-14 19:34:13 -0700550 def first(self):
551 if self._idList:
Zack Williams045b63d2019-01-22 16:30:57 -0700552 model = make_ORMWrapper(
553 self._stub.invoke(
554 "Get%s" % self._modelName, self._stub.make_ID(id=self._idList[0])
555 ),
556 self._stub,
557 )
Scott Baker8c7267d2017-03-14 19:34:13 -0700558 return model
559 else:
560 return None
561
Scott Bakerc59f1bc2017-12-04 16:55:05 -0800562 def add(self, model):
563 if not self._writeable:
564 raise Exception("Only ManyToMany lists are writeable")
565
566 if isinstance(model, int):
567 id = model
568 else:
569 if not model.id:
570 raise Exception("Model %s has no id" % model)
571 id = model.id
572
573 self._idList.append(id)
574
575 def remove(self, model):
576 if not self._writeable:
577 raise Exception("Only ManyToMany lists are writeable")
578
579 if isinstance(model, int):
580 id = model
581 else:
582 if not model.id:
583 raise Exception("Model %s has no id" % model)
584 id = model.id
585
586 self._idList.remove(id)
587
Zack Williams045b63d2019-01-22 16:30:57 -0700588
Scott Baker96b995a2017-02-15 16:21:12 -0800589class ORMObjectManager(object):
590 """ Manages a remote list of objects """
591
Scott Bakerac2f2b52017-02-21 14:53:23 -0800592 # constants better agree with common.proto
Scott Bakerea1f4d02018-12-17 10:21:50 -0800593 DEFAULT = 0
594 ALL = 1
Scott Bakerbae9d842017-03-21 10:44:10 -0700595 SYNCHRONIZER_DIRTY_OBJECTS = 2
596 SYNCHRONIZER_DELETED_OBJECTS = 3
597 SYNCHRONIZER_DIRTY_POLICIES = 4
598 SYNCHRONIZER_DELETED_POLICIES = 5
Scott Bakerac2f2b52017-02-21 14:53:23 -0800599
Scott Bakerea1f4d02018-12-17 10:21:50 -0800600 def __init__(self, stub, modelName, packageName, kind=0):
Scott Baker96b995a2017-02-15 16:21:12 -0800601 self._stub = stub
602 self._modelName = modelName
603 self._packageName = packageName
Scott Bakerea1f4d02018-12-17 10:21:50 -0800604 self._kind = kind
Scott Baker96b995a2017-02-15 16:21:12 -0800605
606 def wrap_single(self, obj):
Scott Baker22796cc2017-02-23 16:53:34 -0800607 return make_ORMWrapper(obj, self._stub)
Scott Baker96b995a2017-02-15 16:21:12 -0800608
609 def wrap_list(self, obj):
Zack Williams045b63d2019-01-22 16:30:57 -0700610 result = []
Scott Baker96b995a2017-02-15 16:21:12 -0800611 for item in obj.items:
Scott Baker22796cc2017-02-23 16:53:34 -0800612 result.append(make_ORMWrapper(item, self._stub))
Scott Bakerb05393b2017-03-01 14:59:55 -0800613 return ORMQuerySet(result)
Scott Baker96b995a2017-02-15 16:21:12 -0800614
615 def all(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700616 if self._kind == self.DEFAULT:
617 return self.wrap_list(
618 self._stub.invoke("List%s" % self._modelName, self._stub.make_empty())
619 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800620 else:
621 return self.filter()
Scott Baker96b995a2017-02-15 16:21:12 -0800622
Scott Baker22796cc2017-02-23 16:53:34 -0800623 def first(self):
Scott Bakerea1f4d02018-12-17 10:21:50 -0800624 objs = self.all()
Scott Baker22796cc2017-02-23 16:53:34 -0800625 if not objs:
626 return None
627 return objs[0]
628
Scott Bakerac2f2b52017-02-21 14:53:23 -0800629 def filter(self, **kwargs):
630 q = self._stub.make_Query()
Scott Bakerea1f4d02018-12-17 10:21:50 -0800631 q.kind = self._kind
Scott Bakerac2f2b52017-02-21 14:53:23 -0800632
633 for (name, val) in kwargs.items():
634 el = q.elements.add()
635
636 if name.endswith("__gt"):
637 name = name[:-4]
638 el.operator = el.GREATER_THAN
639 elif name.endswith("__gte"):
640 name = name[:-5]
641 el.operator = el.GREATER_THAN_OR_EQUAL
642 elif name.endswith("__lt"):
643 name = name[:-4]
644 el.operator = el.LESS_THAN
645 elif name.endswith("__lte"):
646 name = name[:-5]
647 el.operator = el.LESS_THAN_OR_EQUAL
Scott Bakere1607b82018-09-20 14:10:59 -0700648 elif name.endswith("__iexact"):
649 name = name[:-8]
650 el.operator = el.IEXACT
Scott Bakerac2f2b52017-02-21 14:53:23 -0800651 else:
652 el.operator = el.EQUAL
653
654 el.name = name
655 if isinstance(val, int):
656 el.iValue = val
657 else:
658 el.sValue = val
659
660 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
661
662 def filter_special(self, kind):
663 q = self._stub.make_Query()
664 q.kind = kind
665 return self.wrap_list(self._stub.invoke("Filter%s" % self._modelName, q))
666
Scott Baker22796cc2017-02-23 16:53:34 -0800667 def get(self, **kwargs):
Zack Williams5c2ea232019-01-30 15:23:01 -0700668 if list(kwargs.keys()) == ["id"]:
Scott Baker22796cc2017-02-23 16:53:34 -0800669 # the fast and easy case, look it up by id
Zack Williams045b63d2019-01-22 16:30:57 -0700670 return self.wrap_single(
671 self._stub.invoke(
672 "Get%s" % self._modelName, self._stub.make_ID(id=kwargs["id"])
673 )
674 )
Scott Baker22796cc2017-02-23 16:53:34 -0800675 else:
676 # the slightly more difficult case, filter and return the first item
677 objs = self.filter(**kwargs)
678 return objs[0]
Scott Baker96b995a2017-02-15 16:21:12 -0800679
680 def new(self, **kwargs):
Zack Williams045b63d2019-01-22 16:30:57 -0700681 if self._kind != ORMObjectManager.DEFAULT:
682 raise Exception(
683 "Creating objects is only supported by the DEFAULT object manager"
684 )
Scott Bakerea1f4d02018-12-17 10:21:50 -0800685
Scott Bakeraa556b02017-03-07 16:07:34 -0800686 cls = self._stub.all_grpc_classes[self._modelName]
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700687 o = make_ORMWrapper(cls(), self._stub, is_new=True)
Zack Williams045b63d2019-01-22 16:30:57 -0700688 for (k, v) in kwargs.items():
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700689 setattr(o, k, v)
Scott Baker5b7fba02018-10-17 08:46:46 -0700690 o.recompute_initial()
Scott Bakerfe42a6f2017-03-18 09:11:31 -0700691 return o
Scott Baker96b995a2017-02-15 16:21:12 -0800692
Zack Williams045b63d2019-01-22 16:30:57 -0700693
Scott Baker96b995a2017-02-15 16:21:12 -0800694class ORMModelClass(object):
695 def __init__(self, stub, model_name, package_name):
Scott Baker22796cc2017-02-23 16:53:34 -0800696 self.model_name = model_name
Scott Bakeraa556b02017-03-07 16:07:34 -0800697 self._stub = stub
Scott Baker96b995a2017-02-15 16:21:12 -0800698 self.objects = ORMObjectManager(stub, model_name, package_name)
Zack Williams045b63d2019-01-22 16:30:57 -0700699 self.deleted_objects = ORMObjectManager(
700 stub,
701 model_name,
702 package_name,
703 ORMObjectManager.SYNCHRONIZER_DELETED_OBJECTS,
704 )
Scott Baker96b995a2017-02-15 16:21:12 -0800705
Scott Bakerbb81e152017-03-02 15:28:36 -0800706 @property
Scott Baker22796cc2017-02-23 16:53:34 -0800707 def __name__(self):
708 return self.model_name
709
Scott Bakeraa556b02017-03-07 16:07:34 -0800710 @property
711 def content_type_id(self):
712 return self._stub.reverse_content_type_map[self.model_name]
713
Scott Baker8a6d91f2017-03-22 11:23:11 -0700714 def __call__(self, *args, **kwargs):
715 return self.objects.new(*args, **kwargs)
716
Zack Williams045b63d2019-01-22 16:30:57 -0700717
Scott Baker96b995a2017-02-15 16:21:12 -0800718class ORMStub(object):
Zack Williams045b63d2019-01-22 16:30:57 -0700719 def __init__(
720 self,
721 stub,
722 protos,
723 package_name,
724 invoker=None,
725 caller_kind="grpcapi",
726 empty=None,
727 enable_backoff=True,
728 restart_on_disconnect=False,
729 ):
Scott Baker96b995a2017-02-15 16:21:12 -0800730 self.grpc_stub = stub
Scott Bakerb96ba432018-02-26 09:53:48 -0800731 self.protos = protos
732 self.common_protos = protos.common__pb2
Scott Baker96b995a2017-02-15 16:21:12 -0800733 self.all_model_names = []
Scott Bakeraa556b02017-03-07 16:07:34 -0800734 self.all_grpc_classes = {}
735 self.content_type_map = {}
736 self.reverse_content_type_map = {}
Scott Bakeref8d85d2017-02-21 16:44:28 -0800737 self.invoker = invoker
Scott Baker22796cc2017-02-23 16:53:34 -0800738 self.caller_kind = caller_kind
Scott Baker500f8c72017-05-19 09:41:50 -0700739 self.enable_backoff = enable_backoff
Scott Bakerb06e3e02017-12-12 11:05:53 -0800740 self.restart_on_disconnect = restart_on_disconnect
Scott Baker96b995a2017-02-15 16:21:12 -0800741
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700742 if not empty:
Scott Bakerb96ba432018-02-26 09:53:48 -0800743 empty = self.protos.google_dot_protobuf_dot_empty__pb2.Empty
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700744 self._empty = empty
745
Scott Baker96b995a2017-02-15 16:21:12 -0800746 for name in dir(stub):
Zack Williams045b63d2019-01-22 16:30:57 -0700747 if name.startswith("Get"):
748 model_name = name[3:]
749 setattr(self, model_name, ORMModelClass(self, model_name, package_name))
Scott Baker96b995a2017-02-15 16:21:12 -0800750
Zack Williams045b63d2019-01-22 16:30:57 -0700751 self.all_model_names.append(model_name)
Scott Baker96b995a2017-02-15 16:21:12 -0800752
Zack Williams045b63d2019-01-22 16:30:57 -0700753 grpc_class = getattr(self.protos, model_name)
754 self.all_grpc_classes[model_name] = grpc_class
Scott Bakeraa556b02017-03-07 16:07:34 -0800755
Zack Williams045b63d2019-01-22 16:30:57 -0700756 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions._FindExtensionByName(
757 "xos.contentTypeId"
758 )
759 if ct:
760 ct = grpc_class.DESCRIPTOR.GetOptions().Extensions[ct]
761 if ct:
762 self.content_type_map[ct] = model_name
763 self.reverse_content_type_map[model_name] = ct
Scott Bakeraa556b02017-03-07 16:07:34 -0800764
765 def genericForeignKeyResolve(self, content_type_id, id):
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700766 if content_type_id.endswith("_decl"):
767 content_type_id = content_type_id[:-5]
768
769 if content_type_id not in self.content_type_map:
Zack Williams045b63d2019-01-22 16:30:57 -0700770 raise ORMGenericContentNotFoundException(
771 "Content_type %s not found in self.content_type_map" % content_type_id
772 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700773
Scott Bakeraa556b02017-03-07 16:07:34 -0800774 model_name = self.content_type_map[content_type_id]
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700775
Scott Bakeraa556b02017-03-07 16:07:34 -0800776 model = getattr(self, model_name)
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700777 objs = model.objects.filter(id=id)
778 if not objs:
Zack Williams045b63d2019-01-22 16:30:57 -0700779 raise ORMGenericObjectNotFoundException(
780 "Object %s of model %s was not found" % (id, model_name)
781 )
Scott Bakerd0f1dc12018-04-23 12:05:32 -0700782
Scott Bakeraa556b02017-03-07 16:07:34 -0800783 return model.objects.get(id=id)
784
Scott Baker22796cc2017-02-23 16:53:34 -0800785 def add_default_metadata(self, metadata):
Zack Williams045b63d2019-01-22 16:30:57 -0700786 default_metadata = [("caller_kind", self.caller_kind)]
Scott Baker22796cc2017-02-23 16:53:34 -0800787
Scott Bakerd8246712018-07-12 18:08:31 -0700788 # introspect to see if we're running from a synchronizer thread
789 if getattr(threading.current_thread(), "is_sync_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700790 default_metadata.append(("is_sync_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700791
792 # introspect to see if we're running from a model_policy thread
793 if getattr(threading.current_thread(), "is_policy_thread", False):
Zack Williams045b63d2019-01-22 16:30:57 -0700794 default_metadata.append(("is_policy_save", "1"))
Scott Bakerd8246712018-07-12 18:08:31 -0700795
Scott Baker22796cc2017-02-23 16:53:34 -0800796 # build up a list of metadata keys we already have
Zack Williams045b63d2019-01-22 16:30:57 -0700797 md_keys = [x[0] for x in metadata]
Scott Baker22796cc2017-02-23 16:53:34 -0800798
799 # add any defaults that we don't already have
800 for md in default_metadata:
801 if md[0] not in md_keys:
Zack Williams045b63d2019-01-22 16:30:57 -0700802 metadata.append((md[0], md[1]))
Scott Baker22796cc2017-02-23 16:53:34 -0800803
Scott Baker57c74822017-02-23 11:13:04 -0800804 def invoke(self, name, request, metadata=[]):
Scott Baker22796cc2017-02-23 16:53:34 -0800805 self.add_default_metadata(metadata)
806
Scott Bakeref8d85d2017-02-21 16:44:28 -0800807 if self.invoker:
808 # Hook in place to call Chameleon's invoke method, as soon as we
809 # have rewritten the synchronizer to use reactor.
Zack Williams045b63d2019-01-22 16:30:57 -0700810 return self.invoker.invoke(
811 self.grpc_stub.__class__, name, request, metadata={}
812 ).result[0]
Scott Baker500f8c72017-05-19 09:41:50 -0700813 elif self.enable_backoff:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800814 # Our own retry mechanism. This works fine if there is a temporary
815 # failure in connectivity, but does not re-download gRPC schema.
Scott Baker500f8c72017-05-19 09:41:50 -0700816 import grpc
Zack Williams045b63d2019-01-22 16:30:57 -0700817
Scott Bakerb06e3e02017-12-12 11:05:53 -0800818 backoff = [0.5, 1, 2, 4, 8]
Scott Bakeref8d85d2017-02-21 16:44:28 -0800819 while True:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800820 try:
821 method = getattr(self.grpc_stub, name)
Scott Baker57c74822017-02-23 11:13:04 -0800822 return method(request, metadata=metadata)
Zack Williams045b63d2019-01-22 16:30:57 -0700823 except grpc._channel._Rendezvous as e:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800824 code = e.code()
825 if code == grpc.StatusCode.UNAVAILABLE:
Scott Bakerb06e3e02017-12-12 11:05:53 -0800826 if self.restart_on_disconnect:
827 # This is a blunt technique... We lost connectivity to the core, and we don't know that
828 # the core is still serving up the same models it was when we established connectivity,
829 # so restart the synchronizer.
830 # TODO: Hash check on the core models to tell if something changed would be better.
Zack Williams045b63d2019-01-22 16:30:57 -0700831 os.execv(sys.executable, ["python"] + sys.argv)
Scott Bakeref8d85d2017-02-21 16:44:28 -0800832 if not backoff:
833 raise Exception("No more retries on %s" % name)
834 time.sleep(backoff.pop(0))
835 else:
836 raise
Zack Williams045b63d2019-01-22 16:30:57 -0700837 except BaseException:
Scott Bakeref8d85d2017-02-21 16:44:28 -0800838 raise
Scott Baker500f8c72017-05-19 09:41:50 -0700839 else:
840 method = getattr(self.grpc_stub, name)
841 return method(request, metadata=metadata)
842
Scott Baker96b995a2017-02-15 16:21:12 -0800843 def make_ID(self, id):
Scott Bakerb96ba432018-02-26 09:53:48 -0800844 return getattr(self.common_protos, "ID")(id=id)
Scott Bakerf0ee0dc2017-05-15 10:10:05 -0700845
846 def make_empty(self):
847 return self._empty()
Scott Baker96b995a2017-02-15 16:21:12 -0800848
Scott Bakerac2f2b52017-02-21 14:53:23 -0800849 def make_Query(self):
Scott Bakerb96ba432018-02-26 09:53:48 -0800850 return getattr(self.common_protos, "Query")()
Scott Bakerac2f2b52017-02-21 14:53:23 -0800851
Scott Bakerf6145a22017-03-29 14:50:25 -0700852 def listObjects(self):
853 return self.all_model_names
854
Zack Williams045b63d2019-01-22 16:30:57 -0700855
Scott Baker22796cc2017-02-23 16:53:34 -0800856def register_convenience_wrapper(class_name, wrapper):
857 global convenience_wrappers
Scott Baker96b995a2017-02-15 16:21:12 -0800858
Scott Baker22796cc2017-02-23 16:53:34 -0800859 convenience_wrappers[class_name] = wrapper
860
Zack Williams045b63d2019-01-22 16:30:57 -0700861
Scott Baker22796cc2017-02-23 16:53:34 -0800862def make_ORMWrapper(wrapped_class, *args, **kwargs):
Scott Baker2f314d52018-08-24 08:31:19 -0700863 cls = None
864
865 if (not cls) and wrapped_class.__class__.__name__ in convenience_wrappers:
Scott Baker22796cc2017-02-23 16:53:34 -0800866 cls = convenience_wrappers[wrapped_class.__class__.__name__]
Scott Baker2f314d52018-08-24 08:31:19 -0700867
Zack Williams045b63d2019-01-22 16:30:57 -0700868 if not cls:
Scott Baker2f314d52018-08-24 08:31:19 -0700869 # Search the list of class names for this model to see if we have any applicable wrappers. The list is always
870 # sorted from most specific to least specific, so the first one we find will automatically be the most relevant
871 # one. If we don't find any, then default to ORMWrapper
872
873 # Note: Only works on objects that have been fetched from the server, not objects that are created on the
874 # client. This is because wrapped_class.class_names is filled in by the server.
875
876 # TODO(smbaker): Ought to be able to make this work with newly created objects after they are saved.
877
878 for name in wrapped_class.class_names.split(","):
879 if name in convenience_wrappers:
880 cls = convenience_wrappers[name]
881
Zack Williams045b63d2019-01-22 16:30:57 -0700882 if not cls:
Scott Baker22796cc2017-02-23 16:53:34 -0800883 cls = ORMWrapper
884
885 return cls(wrapped_class, *args, **kwargs)
886
Zack Williams045b63d2019-01-22 16:30:57 -0700887
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200888def import_convenience_methods():
Scott Baker8203dc62019-03-14 16:16:19 -0700889 # The ORM has several built-in convenience methods that are contained here
890 lib_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
891 base_convenience_dir = os.path.join(lib_dir, "convenience")
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200892
Scott Baker8203dc62019-03-14 16:16:19 -0700893 # Service convenience methods are placed here during dynamicload
894 service_convenience_dir = "/var/run/xosapi/convenience"
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200895
Scott Baker8203dc62019-03-14 16:16:19 -0700896 for api_convenience_dir in [base_convenience_dir, service_convenience_dir]:
897 log.info("Loading convenience methods", api_convenience_dir=api_convenience_dir)
898
899 if not os.path.exists(api_convenience_dir):
900 log.info("No convenience methods found", api_convenience_dir=api_convenience_dir)
901 continue
902
903 for file in os.listdir(api_convenience_dir):
904 if file.endswith(".py") and "test" not in file:
905 pathname = os.path.join(api_convenience_dir, file)
906 try:
907 log.debug("Loading: %s" % file)
908 imp.load_source(file[:-3], pathname)
909 except Exception:
910 log.exception(
911 "Cannot import api convenience method for: %s, %s"
912 % (file[:-3], pathname)
913 )