blob: 4e1023b2007a2a86f6962b14b4db69213c1d1c3c [file] [log] [blame]
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001#
Zsolt Haraszti3eb27a52017-01-03 21:56:48 -08002# Copyright 2017 the original author or authors.
Zsolt Harasztidafefe12016-11-14 21:29:58 -08003#
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#
Zsolt Harasztidafefe12016-11-14 21:29:58 -080016from copy import copy
17
18from jsonpatch import JsonPatch
19from jsonpatch import make_patch
20
21from common.utils.json_format import MessageToDict
22from voltha.core.config.config_branch import ConfigBranch
alshabibfa007ce2017-02-22 22:19:51 -080023from voltha.core.config.config_event_bus import ConfigEventBus
Zsolt Harasztidafefe12016-11-14 21:29:58 -080024from voltha.core.config.config_proxy import CallbackType, ConfigProxy
25from voltha.core.config.config_rev import is_proto_message, children_fields, \
26 ConfigRevision, access_rights
Zsolt Harasztib7067842016-11-22 18:11:53 -080027from voltha.core.config.config_rev_persisted import PersistedConfigRevision
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080028from voltha.core.config.merge_3way import merge_3way
Zsolt Harasztidafefe12016-11-14 21:29:58 -080029from voltha.protos import third_party
30from voltha.protos import meta_pb2
31
32
Zsolt Harasztidafefe12016-11-14 21:29:58 -080033def message_to_dict(m):
34 return MessageToDict(m, True, True, False)
35
36
37def check_access_violation(new_msg, old_msg):
38 """Raise ValueError if attempt is made to change a read-only field"""
39 access_map = access_rights(new_msg.__class__)
40 violated_fields = []
41 for field_name, access in access_map.iteritems():
42 if access == meta_pb2.READ_ONLY:
43 if getattr(new_msg, field_name) != getattr(old_msg, field_name):
44 violated_fields.append(field_name)
45 if violated_fields:
46 raise ValueError('Cannot change read-only field(s) %s' %
47 ', '.join('"%s"' % f for f in violated_fields))
48
49
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080050def find_rev_by_key(revs, keyname, value):
51 for i, rev in enumerate(revs):
52 if getattr(rev._config._data, keyname) == value:
53 return i, rev
54 raise KeyError('key {}={} not found'.format(keyname, value))
55
56
Zsolt Harasztidafefe12016-11-14 21:29:58 -080057class ConfigNode(object):
58 """
59 Represents a configuration node which can hold a number of revisions
60 of the configuration for this node.
61 When the configuration changes, the new version is appended to the
62 node.
63 Initial data must be a protobuf message and it will determine the type of
64 this node.
65 """
66 __slots__ = (
Zsolt Harasztib7067842016-11-22 18:11:53 -080067 '_root', # ref to root node
Zsolt Harasztidafefe12016-11-14 21:29:58 -080068 '_type', # node type, as __class__ of protobuf message
69 '_branches', # dict of transaction branches and a default (committed)
70 # branch
71 '_tags', # dict of tag-name to ref of ConfigRevision
72 '_proxy', # ref to proxy observer or None if no proxy assigned
alshabibfa007ce2017-02-22 22:19:51 -080073 '_event_bus', # ref to event_bus or None if no event bus is assigned
Zsolt Harasztib7067842016-11-22 18:11:53 -080074 '_auto_prune'
Zsolt Harasztidafefe12016-11-14 21:29:58 -080075 )
76
Zsolt Harasztib7067842016-11-22 18:11:53 -080077 def __init__(self, root, initial_data, auto_prune=True, txid=None):
78 self._root = root
Zsolt Harasztidafefe12016-11-14 21:29:58 -080079 self._branches = {}
80 self._tags = {}
81 self._proxy = None
alshabibfa007ce2017-02-22 22:19:51 -080082 self._event_bus = None
Zsolt Harasztib7067842016-11-22 18:11:53 -080083 self._auto_prune = auto_prune
Zsolt Harasztidafefe12016-11-14 21:29:58 -080084
Zsolt Harasztib7067842016-11-22 18:11:53 -080085 if isinstance(initial_data, type):
86 self._type = initial_data
87 elif is_proto_message(initial_data):
88 self._type = initial_data.__class__
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080089 copied_data = initial_data.__class__()
90 copied_data.CopyFrom(initial_data)
91 self._initialize(copied_data, txid)
Zsolt Harasztib7067842016-11-22 18:11:53 -080092 else:
93 raise NotImplementedError()
Zsolt Harasztidafefe12016-11-14 21:29:58 -080094
Zsolt Harasztib7067842016-11-22 18:11:53 -080095 def _mknode(self, *args, **kw):
96 return ConfigNode(self._root, *args, **kw)
97
98 def _mkrev(self, *args, **kw):
99 return self._root.mkrev(*args, **kw)
100
101 def _initialize(self, data, txid):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800102 # separate external children data away from locally stored data
103 # based on child_node annotations in protobuf
104 children = {}
105 for field_name, field in children_fields(self._type).iteritems():
106 field_value = getattr(data, field_name)
107 if field.is_container:
108 if field.key:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800109 keys_seen = set()
110 children[field_name] = lst = []
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800111 for v in field_value:
Zsolt Harasztib7067842016-11-22 18:11:53 -0800112 rev = self._mknode(v, txid=txid).latest
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800113 key = getattr(v, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800114 if key in keys_seen:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800115 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800116 lst.append(rev)
117 keys_seen.add(key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800118 else:
119 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800120 self._mknode(v, txid=txid).latest for v in field_value]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800121 else:
122 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800123 self._mknode(field_value, txid=txid).latest]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800124 data.ClearField(field_name)
125
Zsolt Harasztib7067842016-11-22 18:11:53 -0800126 branch = ConfigBranch(self, auto_prune=self._auto_prune)
127 rev = self._mkrev(branch, data, children)
128 self._make_latest(branch, rev)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800129 self._branches[txid] = branch
130
131 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ accessors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
132 # these convenience short-cuts only work for the committed branch
133
134 @property
135 def revisions(self):
136 return [r._hash for r in self._branches[None]._revs.itervalues()]
137
138 @property
139 def latest(self):
140 return self._branches[None]._latest
141
142 def __getitem__(self, hash):
143 return self._branches[None]._revs[hash]
144
145 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ get operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
146
147 def get(self, path=None, hash=None, depth=0, deep=False, txid=None):
148
149 # depth preparation
150 if deep:
151 depth = -1
152
153 # path preparation
154 path = '' if path is None else path
155 while path.startswith('/'):
156 path = path[1:]
157
158 # determine branch; if lookup fails, it is ok to use default branch
159 branch = self._branches.get(txid, None) or self._branches[None]
160
161 # determine rev
162 if hash is not None:
163 rev = branch._revs[hash]
164 else:
165 rev = branch.latest
166
167 return self._get(rev, path, depth)
168
169 def _get(self, rev, path, depth):
170
171 if not path:
172 return self._do_get(rev, depth)
173
174 # ... otherwise
175 name, _, path = path.partition('/')
176 field = children_fields(self._type)[name]
177 if field.is_container:
178 if field.key:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800179 children = rev._children[name]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800180 if path:
181 # need to escalate further
182 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800183 key = field.key_from_str(key)
184 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800185 child_node = child_rev.node
186 return child_node._get(child_rev, path, depth)
187 else:
188 # we are the node of interest
189 response = []
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800190 for child_rev in children:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800191 child_node = child_rev.node
192 value = child_node._do_get(child_rev, depth)
193 response.append(value)
194 return response
195 else:
196 if path:
197 raise LookupError(
198 'Cannot index into container with no key defined')
199 response = []
200 for child_rev in rev._children[name]:
201 child_node = child_rev.node
202 value = child_node._do_get(child_rev, depth)
203 response.append(value)
204 return response
205 else:
206 child_rev = rev._children[name][0]
207 child_node = child_rev.node
208 return child_node._get(child_rev, path, depth)
209
210 def _do_get(self, rev, depth):
211 msg = rev.get(depth)
212 if self._proxy is not None:
213 msg = self._proxy.invoke_callbacks(CallbackType.GET, msg)
214 return msg
215
216 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ update operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
217
218 def update(self, path, data, strict=False, txid=None, mk_branch=None):
219
220 while path.startswith('/'):
221 path = path[1:]
222
223 try:
224 branch = self._branches[txid]
225 except KeyError:
226 branch = mk_branch(self)
227
228 if not path:
229 return self._do_update(branch, data, strict)
230
231 rev = branch._latest # change is always made to the latest
232 name, _, path = path.partition('/')
233 field = children_fields(self._type)[name]
234 if field.is_container:
235 if not path:
236 raise ValueError('Cannot update a list')
237 if field.key:
238 key, _, path = path.partition('/')
239 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800240 children = copy(rev._children[name])
241 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800242 child_node = child_rev.node
243 new_child_rev = child_node.update(
244 path, data, strict, txid, mk_branch)
245 if new_child_rev.hash == child_rev.hash:
246 # no change, we can return
247 return branch._latest
248 if getattr(new_child_rev.data, field.key) != key:
249 raise ValueError('Cannot change key field')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800250 children[idx] = new_child_rev
251 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800252 self._make_latest(branch, rev)
253 return rev
254 else:
255 raise ValueError('Cannot index into container with no keys')
256
257 else:
258 child_rev = rev._children[name][0]
259 child_node = child_rev.node
260 new_child_rev = child_node.update(
261 path, data, strict, txid, mk_branch)
262 rev = rev.update_children(name, [new_child_rev], branch)
263 self._make_latest(branch, rev)
264 return rev
265
266 def _do_update(self, branch, data, strict):
267 if not isinstance(data, self._type):
268 raise ValueError(
269 '"{}" is not a valid data type for this node'.format(
270 data.__class__.__name__))
271 self._test_no_children(data)
272 if self._proxy is not None:
273 self._proxy.invoke_callbacks(CallbackType.PRE_UPDATE, data)
274
275 if branch._latest.data != data:
276 if strict:
277 # check if attempt is made to change read-only field
278 check_access_violation(data, branch._latest.data)
279 rev = branch._latest.update_data(data, branch)
280 self._make_latest(branch, rev,
281 ((CallbackType.POST_UPDATE, rev.data),))
282 return rev
283 else:
284 return branch._latest
285
286 def _make_latest(self, branch, rev, change_announcements=()):
287 branch._latest = rev
288 if rev.hash not in branch._revs:
289 branch._revs[rev.hash] = rev
290
291 # announce only if this is main branch
alshabibfa007ce2017-02-22 22:19:51 -0800292 if change_announcements and branch._txid is None:
293
294 if self._proxy is not None:
295 for change_type, data in change_announcements:
296 # since the callback may operate on the config tree,
297 # we have to defer the execution of the callbacks till
298 # the change is propagated to the root, then root will
299 # call the callbacks
300 self._root.enqueue_callback(
301 self._proxy.invoke_callbacks,
302 change_type,
303 data,
304 proceed_on_errors=1,
305 )
306
307
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800308 for change_type, data in change_announcements:
Zsolt Haraszti66862032016-11-28 14:28:39 -0800309 self._root.enqueue_callback(
alshabibfa007ce2017-02-22 22:19:51 -0800310 self._mk_event_bus().advertise,
Zsolt Haraszti66862032016-11-28 14:28:39 -0800311 change_type,
312 data,
alshabibfa007ce2017-02-22 22:19:51 -0800313 hash=rev.hash
Zsolt Haraszti66862032016-11-28 14:28:39 -0800314 )
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800315
316 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
317
318 def add(self, path, data, txid=None, mk_branch=None):
319 while path.startswith('/'):
320 path = path[1:]
321 if not path:
322 raise ValueError('Cannot add to non-container node')
323
324 try:
325 branch = self._branches[txid]
326 except KeyError:
327 branch = mk_branch(self)
328
329 rev = branch._latest # change is always made to latest
330 name, _, path = path.partition('/')
331 field = children_fields(self._type)[name]
332 if field.is_container:
333 if not path:
334 # we do need to add a new child to the field
335 if field.key:
336 if self._proxy is not None:
337 self._proxy.invoke_callbacks(
338 CallbackType.PRE_ADD, data)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800339 children = copy(rev._children[name])
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800340 key = getattr(data, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800341 try:
342 find_rev_by_key(children, field.key, key)
343 except KeyError:
344 pass
345 else:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800346 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800347 child_rev = self._mknode(data).latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800348 children.append(child_rev)
349 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800350 self._make_latest(branch, rev,
351 ((CallbackType.POST_ADD, data),))
352 return rev
353 else:
354 # adding to non-keyed containers not implemented yet
355 raise ValueError('Cannot add to non-keyed container')
356 else:
357 if field.key:
358 # need to escalate
359 key, _, path = path.partition('/')
360 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800361 children = copy(rev._children[name])
362 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800363 child_node = child_rev.node
364 new_child_rev = child_node.add(path, data, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800365 children[idx] = new_child_rev
366 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800367 self._make_latest(branch, rev)
368 return rev
369 else:
370 raise ValueError(
371 'Cannot index into container with no keys')
372 else:
373 raise ValueError('Cannot add to non-container field')
374
375 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
376
377 def remove(self, path, txid=None, mk_branch=None):
378 while path.startswith('/'):
379 path = path[1:]
380 if not path:
381 raise ValueError('Cannot remove from non-container node')
382
383 try:
384 branch = self._branches[txid]
385 except KeyError:
386 branch = mk_branch(self)
387
388 rev = branch._latest # change is always made to latest
389 name, _, path = path.partition('/')
390 field = children_fields(self._type)[name]
391 if field.is_container:
392 if not path:
393 raise ValueError("Cannot remove without a key")
394 if field.key:
395 key, _, path = path.partition('/')
396 key = field.key_from_str(key)
397 if path:
398 # need to escalate
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800399 children = copy(rev._children[name])
400 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800401 child_node = child_rev.node
402 new_child_rev = child_node.remove(path, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800403 children[idx] = new_child_rev
404 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800405 self._make_latest(branch, rev)
406 return rev
407 else:
408 # need to remove from this very node
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800409 children = copy(rev._children[name])
410 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800411 if self._proxy is not None:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800412 data = child_rev.data
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800413 self._proxy.invoke_callbacks(
414 CallbackType.PRE_REMOVE, data)
415 post_anno = ((CallbackType.POST_REMOVE, data),)
416 else:
alshabibfa007ce2017-02-22 22:19:51 -0800417 post_anno = ((CallbackType.POST_REMOVE, child_rev.data),)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800418 del children[idx]
419 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800420 self._make_latest(branch, rev, post_anno)
421 return rev
422 else:
423 raise ValueError('Cannot remove from non-keyed container')
424 else:
425 raise ValueError('Cannot remove non-conatiner field')
426
427 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
428
429 def _mk_txbranch(self, txid):
430 branch_point = self._branches[None].latest
431 branch = ConfigBranch(self, txid, branch_point)
432 self._branches[txid] = branch
433 return branch
434
435 def _del_txbranch(self, txid):
436 del self._branches[txid]
437
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800438 def _merge_txbranch(self, txid, dry_run=False):
439 """
440 Make latest in branch to be latest in the common branch, but only
441 if no conflict is detected. Conflict is where the txbranch branch
442 point no longer matches the latest in the default branch. This has
443 to be verified recursively.
444 """
445
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800446 def merge_child(child_rev):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800447 child_branch = child_rev._branch
448 if child_branch._txid == txid:
449 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
450 return child_rev
451
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800452 src_branch = self._branches[txid]
453 dst_branch = self._branches[None]
454
455 fork_rev = src_branch.origin # rev from which src branch was made
456 src_rev = src_branch.latest # head rev of source branch
457 dst_rev = dst_branch.latest # head rev of target branch
458
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800459 rev, changes = merge_3way(
460 fork_rev, src_rev, dst_rev, merge_child, dry_run)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800461
462 if not dry_run:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800463 self._make_latest(dst_branch, rev, change_announcements=changes)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800464 del self._branches[txid]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800465
466 return rev
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800467
468 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
469
470 def diff(self, hash1, hash2=None, txid=None):
471 branch = self._branches[txid]
472 rev1 = branch[hash1]
473 rev2 = branch[hash2] if hash2 else branch._latest
474 if rev1.hash == rev2.hash:
475 return JsonPatch([])
476 else:
477 dict1 = message_to_dict(rev1.data)
478 dict2 = message_to_dict(rev2.data)
479 return make_patch(dict1, dict2)
480
481 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
482
483 def tag(self, tag, hash=None):
484 branch = self._branches[None] # tag only what has been committed
485 rev = branch._latest if hash is None else branch._revs[hash]
486 self._tags[tag] = rev
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800487 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800488 return self
489
490 @property
491 def tags(self):
492 return sorted(self._tags.iterkeys())
493
494 def by_tag(self, tag):
495 """
496 Return revision based on tag
497 :param tag: previously registered tag value
498 :return: revision object
499 """
500 return self._tags[tag]
501
502 def diff_by_tag(self, tag1, tag2):
503 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
504
505 def delete_tag(self, tag):
506 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800507 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800508
509 def delete_tags(self, *tags):
510 for tag in tags:
511 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800512 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800513
514 def prune_untagged(self):
515 branch = self._branches[None]
516 keep = set(rev.hash for rev in self._tags.itervalues())
517 keep.add(branch._latest.hash)
518 for hash in branch._revs.keys():
519 if hash not in keep:
520 del branch._revs[hash]
521 return self
522
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800523 def persist_tags(self):
524 """
525 Persist tag information to the backend
526 """
527
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800528 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
529
530 def _test_no_children(self, data):
531 for field_name, field in children_fields(self._type).items():
532 field_value = getattr(data, field_name)
533 if field.is_container:
534 if len(field_value):
535 raise NotImplementedError(
536 'Cannot update external children')
537 else:
538 if data.HasField(field_name):
539 raise NotImplementedError(
540 'Cannot update externel children')
541
542 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
543
544 def get_proxy(self, path, exclusive=False):
545 return self._get_proxy(path, self, path, exclusive)
546
547 def _get_proxy(self, path, root, full_path, exclusive):
548 while path.startswith('/'):
549 path = path[1:]
550 if not path:
551 return self._mk_proxy(root, full_path, exclusive)
552
553 # need to escalate
554 rev = self._branches[None]._latest
555 name, _, path = path.partition('/')
556 field = children_fields(self._type)[name]
557 if field.is_container:
558 if not path:
559 raise ValueError('Cannot proxy a container field')
560 if field.key:
561 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800562 key = field.key_from_str(key)
563 children = rev._children[name]
564 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800565 child_node = child_rev.node
566 return child_node._get_proxy(path, root, full_path, exclusive)
567
568 raise ValueError('Cannot index into container with no keys')
569
570 else:
571 child_rev = rev._children[name][0]
572 child_node = child_rev.node
573 return child_node._get_proxy(path, root, full_path, exclusive)
574
575 def _mk_proxy(self, root, full_path, exclusive):
576 if self._proxy is None:
577 self._proxy = ConfigProxy(root, self, full_path, exclusive)
578 else:
579 if self._proxy.exclusive:
580 raise ValueError('Node is already owned exclusively')
581 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800582
alshabibfa007ce2017-02-22 22:19:51 -0800583 def _mk_event_bus(self):
584 if self._event_bus is None:
585 self._event_bus = ConfigEventBus()
586 return self._event_bus
587
Zsolt Harasztib7067842016-11-22 18:11:53 -0800588 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
589
590 def load_latest(self, latest_hash):
591
592 root = self._root
593 kv_store = root._kv_store
594
595 branch = ConfigBranch(self, self._auto_prune)
596 rev = PersistedConfigRevision.load(
597 branch, kv_store, self._type, latest_hash)
598 self._make_latest(branch, rev)
599 self._branches[None] = branch