blob: 6df8b4c3c8c649af3668b854d92f3525a4ee2b15 [file] [log] [blame]
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001#
2# Copyright 2016 the original author or authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16from collections import OrderedDict
17from copy import copy
18
19from jsonpatch import JsonPatch
20from jsonpatch import make_patch
21
22from common.utils.json_format import MessageToDict
23from voltha.core.config.config_branch import ConfigBranch
24from 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 Harasztidafefe12016-11-14 21:29:58 -080028from voltha.protos import third_party
29from voltha.protos import meta_pb2
30
31
32class MergeConflictException(Exception):
33 pass
34
35
36def message_to_dict(m):
37 return MessageToDict(m, True, True, False)
38
39
40def check_access_violation(new_msg, old_msg):
41 """Raise ValueError if attempt is made to change a read-only field"""
42 access_map = access_rights(new_msg.__class__)
43 violated_fields = []
44 for field_name, access in access_map.iteritems():
45 if access == meta_pb2.READ_ONLY:
46 if getattr(new_msg, field_name) != getattr(old_msg, field_name):
47 violated_fields.append(field_name)
48 if violated_fields:
49 raise ValueError('Cannot change read-only field(s) %s' %
50 ', '.join('"%s"' % f for f in violated_fields))
51
52
53class ConfigNode(object):
54 """
55 Represents a configuration node which can hold a number of revisions
56 of the configuration for this node.
57 When the configuration changes, the new version is appended to the
58 node.
59 Initial data must be a protobuf message and it will determine the type of
60 this node.
61 """
62 __slots__ = (
Zsolt Harasztib7067842016-11-22 18:11:53 -080063 '_root', # ref to root node
Zsolt Harasztidafefe12016-11-14 21:29:58 -080064 '_type', # node type, as __class__ of protobuf message
65 '_branches', # dict of transaction branches and a default (committed)
66 # branch
67 '_tags', # dict of tag-name to ref of ConfigRevision
68 '_proxy', # ref to proxy observer or None if no proxy assigned
Zsolt Harasztib7067842016-11-22 18:11:53 -080069 '_auto_prune'
Zsolt Harasztidafefe12016-11-14 21:29:58 -080070 )
71
Zsolt Harasztib7067842016-11-22 18:11:53 -080072 def __init__(self, root, initial_data, auto_prune=True, txid=None):
73 self._root = root
Zsolt Harasztidafefe12016-11-14 21:29:58 -080074 self._branches = {}
75 self._tags = {}
76 self._proxy = None
Zsolt Harasztib7067842016-11-22 18:11:53 -080077 self._auto_prune = auto_prune
Zsolt Harasztidafefe12016-11-14 21:29:58 -080078
Zsolt Harasztib7067842016-11-22 18:11:53 -080079 if isinstance(initial_data, type):
80 self._type = initial_data
81 elif is_proto_message(initial_data):
82 self._type = initial_data.__class__
83 self._initialize(copy(initial_data), txid)
84 else:
85 raise NotImplementedError()
Zsolt Harasztidafefe12016-11-14 21:29:58 -080086
Zsolt Harasztib7067842016-11-22 18:11:53 -080087 def _mknode(self, *args, **kw):
88 return ConfigNode(self._root, *args, **kw)
89
90 def _mkrev(self, *args, **kw):
91 return self._root.mkrev(*args, **kw)
92
93 def _initialize(self, data, txid):
Zsolt Harasztidafefe12016-11-14 21:29:58 -080094 # separate external children data away from locally stored data
95 # based on child_node annotations in protobuf
96 children = {}
97 for field_name, field in children_fields(self._type).iteritems():
98 field_value = getattr(data, field_name)
99 if field.is_container:
100 if field.key:
101 children[field_name] = od = OrderedDict()
102 for v in field_value:
Zsolt Harasztib7067842016-11-22 18:11:53 -0800103 rev = self._mknode(v, txid=txid).latest
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800104 key = getattr(v, field.key)
105 if key in od:
106 raise ValueError('Duplicate key "{}"'.format(key))
107 od[key] = rev
108 else:
109 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800110 self._mknode(v, txid=txid).latest for v in field_value]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800111 else:
112 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800113 self._mknode(field_value, txid=txid).latest]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800114 data.ClearField(field_name)
115
Zsolt Harasztib7067842016-11-22 18:11:53 -0800116 branch = ConfigBranch(self, auto_prune=self._auto_prune)
117 rev = self._mkrev(branch, data, children)
118 self._make_latest(branch, rev)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800119 self._branches[txid] = branch
120
121 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ accessors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122 # these convenience short-cuts only work for the committed branch
123
124 @property
125 def revisions(self):
126 return [r._hash for r in self._branches[None]._revs.itervalues()]
127
128 @property
129 def latest(self):
130 return self._branches[None]._latest
131
132 def __getitem__(self, hash):
133 return self._branches[None]._revs[hash]
134
135 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ get operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
136
137 def get(self, path=None, hash=None, depth=0, deep=False, txid=None):
138
139 # depth preparation
140 if deep:
141 depth = -1
142
143 # path preparation
144 path = '' if path is None else path
145 while path.startswith('/'):
146 path = path[1:]
147
148 # determine branch; if lookup fails, it is ok to use default branch
149 branch = self._branches.get(txid, None) or self._branches[None]
150
151 # determine rev
152 if hash is not None:
153 rev = branch._revs[hash]
154 else:
155 rev = branch.latest
156
157 return self._get(rev, path, depth)
158
159 def _get(self, rev, path, depth):
160
161 if not path:
162 return self._do_get(rev, depth)
163
164 # ... otherwise
165 name, _, path = path.partition('/')
166 field = children_fields(self._type)[name]
167 if field.is_container:
168 if field.key:
169 children_od = rev._children[name]
170 if path:
171 # need to escalate further
172 key, _, path = path.partition('/')
173 child_rev = children_od[field.key_from_str(key)]
174 child_node = child_rev.node
175 return child_node._get(child_rev, path, depth)
176 else:
177 # we are the node of interest
178 response = []
179 for child_rev in children_od.itervalues():
180 child_node = child_rev.node
181 value = child_node._do_get(child_rev, depth)
182 response.append(value)
183 return response
184 else:
185 if path:
186 raise LookupError(
187 'Cannot index into container with no key defined')
188 response = []
189 for child_rev in rev._children[name]:
190 child_node = child_rev.node
191 value = child_node._do_get(child_rev, depth)
192 response.append(value)
193 return response
194 else:
195 child_rev = rev._children[name][0]
196 child_node = child_rev.node
197 return child_node._get(child_rev, path, depth)
198
199 def _do_get(self, rev, depth):
200 msg = rev.get(depth)
201 if self._proxy is not None:
202 msg = self._proxy.invoke_callbacks(CallbackType.GET, msg)
203 return msg
204
205 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ update operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
206
207 def update(self, path, data, strict=False, txid=None, mk_branch=None):
208
209 while path.startswith('/'):
210 path = path[1:]
211
212 try:
213 branch = self._branches[txid]
214 except KeyError:
215 branch = mk_branch(self)
216
217 if not path:
218 return self._do_update(branch, data, strict)
219
220 rev = branch._latest # change is always made to the latest
221 name, _, path = path.partition('/')
222 field = children_fields(self._type)[name]
223 if field.is_container:
224 if not path:
225 raise ValueError('Cannot update a list')
226 if field.key:
227 key, _, path = path.partition('/')
228 key = field.key_from_str(key)
229 children_od = copy(rev._children[name])
230 child_rev = children_od[key]
231 child_node = child_rev.node
232 new_child_rev = child_node.update(
233 path, data, strict, txid, mk_branch)
234 if new_child_rev.hash == child_rev.hash:
235 # no change, we can return
236 return branch._latest
237 if getattr(new_child_rev.data, field.key) != key:
238 raise ValueError('Cannot change key field')
239 children_od[key] = new_child_rev
240 rev = rev.update_children(name, children_od, branch)
241 self._make_latest(branch, rev)
242 return rev
243 else:
244 raise ValueError('Cannot index into container with no keys')
245
246 else:
247 child_rev = rev._children[name][0]
248 child_node = child_rev.node
249 new_child_rev = child_node.update(
250 path, data, strict, txid, mk_branch)
251 rev = rev.update_children(name, [new_child_rev], branch)
252 self._make_latest(branch, rev)
253 return rev
254
255 def _do_update(self, branch, data, strict):
256 if not isinstance(data, self._type):
257 raise ValueError(
258 '"{}" is not a valid data type for this node'.format(
259 data.__class__.__name__))
260 self._test_no_children(data)
261 if self._proxy is not None:
262 self._proxy.invoke_callbacks(CallbackType.PRE_UPDATE, data)
263
264 if branch._latest.data != data:
265 if strict:
266 # check if attempt is made to change read-only field
267 check_access_violation(data, branch._latest.data)
268 rev = branch._latest.update_data(data, branch)
269 self._make_latest(branch, rev,
270 ((CallbackType.POST_UPDATE, rev.data),))
271 return rev
272 else:
273 return branch._latest
274
275 def _make_latest(self, branch, rev, change_announcements=()):
276 branch._latest = rev
277 if rev.hash not in branch._revs:
278 branch._revs[rev.hash] = rev
279
280 # announce only if this is main branch
281 if change_announcements and branch._txid is None and \
282 self._proxy is not None:
283 for change_type, data in change_announcements:
284 self._proxy.invoke_callbacks(
285 change_type, data, proceed_on_errors=1)
286
287 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
288
289 def add(self, path, data, txid=None, mk_branch=None):
290 while path.startswith('/'):
291 path = path[1:]
292 if not path:
293 raise ValueError('Cannot add to non-container node')
294
295 try:
296 branch = self._branches[txid]
297 except KeyError:
298 branch = mk_branch(self)
299
300 rev = branch._latest # change is always made to latest
301 name, _, path = path.partition('/')
302 field = children_fields(self._type)[name]
303 if field.is_container:
304 if not path:
305 # we do need to add a new child to the field
306 if field.key:
307 if self._proxy is not None:
308 self._proxy.invoke_callbacks(
309 CallbackType.PRE_ADD, data)
310 children_od = copy(rev._children[name])
311 key = getattr(data, field.key)
312 if key in children_od:
313 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800314 child_rev = self._mknode(data).latest
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800315 children_od[key] = child_rev
316 rev = rev.update_children(name, children_od, branch)
317 self._make_latest(branch, rev,
318 ((CallbackType.POST_ADD, data),))
319 return rev
320 else:
321 # adding to non-keyed containers not implemented yet
322 raise ValueError('Cannot add to non-keyed container')
323 else:
324 if field.key:
325 # need to escalate
326 key, _, path = path.partition('/')
327 key = field.key_from_str(key)
328 children_od = copy(rev._children[name])
329 child_rev = children_od[key]
330 child_node = child_rev.node
331 new_child_rev = child_node.add(path, data, txid, mk_branch)
332 children_od[key] = new_child_rev
333 rev = rev.update_children(name, children_od, branch)
334 self._make_latest(branch, rev)
335 return rev
336 else:
337 raise ValueError(
338 'Cannot index into container with no keys')
339 else:
340 raise ValueError('Cannot add to non-container field')
341
342 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
343
344 def remove(self, path, txid=None, mk_branch=None):
345 while path.startswith('/'):
346 path = path[1:]
347 if not path:
348 raise ValueError('Cannot remove from non-container node')
349
350 try:
351 branch = self._branches[txid]
352 except KeyError:
353 branch = mk_branch(self)
354
355 rev = branch._latest # change is always made to latest
356 name, _, path = path.partition('/')
357 field = children_fields(self._type)[name]
358 if field.is_container:
359 if not path:
360 raise ValueError("Cannot remove without a key")
361 if field.key:
362 key, _, path = path.partition('/')
363 key = field.key_from_str(key)
364 if path:
365 # need to escalate
366 children_od = copy(rev._children[name])
367 child_rev = children_od[key]
368 child_node = child_rev.node
369 new_child_rev = child_node.remove(path, txid, mk_branch)
370 children_od[key] = new_child_rev
371 rev = rev.update_children(name, children_od, branch)
372 self._make_latest(branch, rev)
373 return rev
374 else:
375 # need to remove from this very node
376 children_od = copy(rev._children[name])
377 if self._proxy is not None:
378 data = children_od[field.key_from_str(key)].data
379 self._proxy.invoke_callbacks(
380 CallbackType.PRE_REMOVE, data)
381 post_anno = ((CallbackType.POST_REMOVE, data),)
382 else:
383 post_anno = ()
384 del children_od[field.key_from_str(key)]
385 rev = rev.update_children(name, children_od, branch)
386 self._make_latest(branch, rev, post_anno)
387 return rev
388 else:
389 raise ValueError('Cannot remove from non-keyed container')
390 else:
391 raise ValueError('Cannot remove non-conatiner field')
392
393 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
394
395 def _mk_txbranch(self, txid):
396 branch_point = self._branches[None].latest
397 branch = ConfigBranch(self, txid, branch_point)
398 self._branches[txid] = branch
399 return branch
400
401 def _del_txbranch(self, txid):
402 del self._branches[txid]
403
404 # def can_txbranch_be_merged(self, txid):
405 # try:
406 # self._merge_txbranch(txid, dry_run=True)
407 # except MergeConflictException:
408 # return False
409 # else:
410 # return True
411
412 def _merge_txbranch(self, txid, dry_run=False):
413 """
414 Make latest in branch to be latest in the common branch, but only
415 if no conflict is detected. Conflict is where the txbranch branch
416 point no longer matches the latest in the default branch. This has
417 to be verified recursively.
418 """
419
420 """
421 A transaction branch can be merged only if none of the following
422 happened with the master branch since the fork rev:
423 - the local data was changed both in the incoming node and in the
424 default branch since the branch point, and they differ now
425 - both branches changed the same children nodes in any way (local or
426 deep)
427 """
428
429 announcements = []
430
431 def _get_od_changes(lst1, lst2):
Zsolt Harasztib7067842016-11-22 18:11:53 -0800432 assert isinstance(lst2, dict)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800433 added_keys = [k for k in lst2.iterkeys() if k not in lst1]
434 removed_keys = [k for k in lst1.iterkeys() if k not in lst2]
435 changed_keys = [k for k in lst1.iterkeys()
436 if k in lst2 and lst1[k].hash != lst2[k].hash]
437 return added_keys, removed_keys, changed_keys
438
439 def _get_changes(lst1, lst2):
Zsolt Harasztib7067842016-11-22 18:11:53 -0800440 if isinstance(lst1, dict):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800441 return _get_od_changes(lst1, lst2)
442 assert isinstance(lst1, list)
443 assert isinstance(lst2, list)
444 set1 = set(lst1)
445 set2 = set(lst2)
446 added = set2.difference(set1)
447 removed = set1.difference(set2)
448 changed = set() # no such thing in plain (unkeyed) lists
449 return added, removed, changed
450
451 def _escalate(child_rev):
452 child_branch = child_rev._branch
453 if child_branch._txid == txid:
454 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
455 return child_rev
456
457 def _escalate_list(src_list):
458 if isinstance(src_list, list):
459 lst = []
460 for child_rev in src_list:
461 lst.append(_escalate(child_rev))
462 return lst
463 else: # OrderedDict
464 od = OrderedDict()
465 for key, child_rev in src_list.iteritems():
466 od[key] = _escalate(child_rev)
467 return od
468
469 def _add(dst, rev_or_key, src):
470 if isinstance(dst, list):
471 dst.append(_escalate(rev_or_key))
472 announcements.append((CallbackType.POST_ADD, rev_or_key.data))
473 else: # OrderedDict key, data is in lst
474 rev = src[rev_or_key]
475 dst[rev_or_key] = _escalate(rev)
476 announcements.append((CallbackType.POST_ADD, rev.data))
477
478 def _remove(dst, rev_or_key):
479 if isinstance(dst, list):
480 dst.remove(rev_or_key)
481 announcements.append((CallbackType.POST_REMOVE, rev_or_key))
482 else:
483 rev = dst[rev_or_key]
484 del dst[rev_or_key]
485 announcements.append((CallbackType.POST_REMOVE, rev.data))
486
487 src_branch = self._branches[txid]
488 dst_branch = self._branches[None]
489
490 fork_rev = src_branch.origin # rev from which src branch was made
491 src_rev = src_branch.latest # head rev of source branch
492 dst_rev = dst_branch.latest # head rev of target branch
493
494 # deal with config data first
495 if dst_rev._config is fork_rev._config:
496 # no change in master, accept src if different
497 config_changed = dst_rev._config != src_rev._config
498 else:
499 if dst_rev._config.hash != src_rev._config.hash:
500 raise MergeConflictException('Config collision')
501 config_changed = True
502
503 new_children = copy(dst_rev._children)
504 for field_name, field in children_fields(self._type).iteritems():
505 fork_list = fork_rev._children[field_name]
506 src_list = src_rev._children[field_name]
507 dst_list = dst_rev._children[field_name]
508 if 0: #dst_list == fork_list:
509 # no change in master, accept src if different
510 if src_list != fork_list:
511 new_children[field_name] = _escalate_list(src_list)
512 else:
513 src_added, src_removed, src_changed = _get_changes(
514 fork_list, src_list)
515 dst_added, dst_removed, dst_changed = _get_changes(
516 fork_list, dst_list)
517
518 lst = copy(new_children[field_name])
519 for to_add in src_added:
520 # we cannot add if it has been added and is different
521 if to_add in dst_added:
522 # this can happen only to keyed containers
Zsolt Harasztib7067842016-11-22 18:11:53 -0800523 assert isinstance(src_list, dict)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800524 if src_list[to_add].hash != dst_list[to_add].hash:
525 raise MergeConflictException(
526 'Cannot add because it has been added and '
527 'different'
528 )
529 _add(lst, to_add, src_list)
530 for to_remove in src_removed:
531 # we cannot remove if it has changed in dst
532 if to_remove in dst_changed:
533 raise MergeConflictException(
534 'Cannot remove because it has changed')
535 if to_remove not in dst_removed:
536 _remove(lst, to_remove)
537 for to_change in src_changed:
538 # we cannot change if it was removed in dst
539 if to_change in dst_removed:
540 raise MergeConflictException(
541 'Cannot change because it has been removed')
542 # change can only be in keyed containers (OrderedDict)
543 lst[to_change] = _escalate(src_list[to_change])
544 new_children[field_name] = lst
545
546 if not dry_run:
547 rev = src_rev if config_changed else dst_rev
548 rev = rev.update_all_children(new_children, dst_branch)
549 if config_changed:
550 announcements.append((CallbackType.POST_UPDATE, rev.data))
551 self._make_latest(dst_branch, rev, announcements)
552 del self._branches[txid]
553 return rev
554
555 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
556
557 def diff(self, hash1, hash2=None, txid=None):
558 branch = self._branches[txid]
559 rev1 = branch[hash1]
560 rev2 = branch[hash2] if hash2 else branch._latest
561 if rev1.hash == rev2.hash:
562 return JsonPatch([])
563 else:
564 dict1 = message_to_dict(rev1.data)
565 dict2 = message_to_dict(rev2.data)
566 return make_patch(dict1, dict2)
567
568 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
569
570 def tag(self, tag, hash=None):
571 branch = self._branches[None] # tag only what has been committed
572 rev = branch._latest if hash is None else branch._revs[hash]
573 self._tags[tag] = rev
574 return self
575
576 @property
577 def tags(self):
578 return sorted(self._tags.iterkeys())
579
580 def by_tag(self, tag):
581 """
582 Return revision based on tag
583 :param tag: previously registered tag value
584 :return: revision object
585 """
586 return self._tags[tag]
587
588 def diff_by_tag(self, tag1, tag2):
589 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
590
591 def delete_tag(self, tag):
592 del self._tags[tag]
593
594 def delete_tags(self, *tags):
595 for tag in tags:
596 del self._tags[tag]
597
598 def prune_untagged(self):
599 branch = self._branches[None]
600 keep = set(rev.hash for rev in self._tags.itervalues())
601 keep.add(branch._latest.hash)
602 for hash in branch._revs.keys():
603 if hash not in keep:
604 del branch._revs[hash]
605 return self
606
607 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
608
609 def _test_no_children(self, data):
610 for field_name, field in children_fields(self._type).items():
611 field_value = getattr(data, field_name)
612 if field.is_container:
613 if len(field_value):
614 raise NotImplementedError(
615 'Cannot update external children')
616 else:
617 if data.HasField(field_name):
618 raise NotImplementedError(
619 'Cannot update externel children')
620
621 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
622
623 def get_proxy(self, path, exclusive=False):
624 return self._get_proxy(path, self, path, exclusive)
625
626 def _get_proxy(self, path, root, full_path, exclusive):
627 while path.startswith('/'):
628 path = path[1:]
629 if not path:
630 return self._mk_proxy(root, full_path, exclusive)
631
632 # need to escalate
633 rev = self._branches[None]._latest
634 name, _, path = path.partition('/')
635 field = children_fields(self._type)[name]
636 if field.is_container:
637 if not path:
638 raise ValueError('Cannot proxy a container field')
639 if field.key:
640 key, _, path = path.partition('/')
641 children_od = rev._children[name]
642 child_rev = children_od[key]
643 child_node = child_rev.node
644 return child_node._get_proxy(path, root, full_path, exclusive)
645
646 raise ValueError('Cannot index into container with no keys')
647
648 else:
649 child_rev = rev._children[name][0]
650 child_node = child_rev.node
651 return child_node._get_proxy(path, root, full_path, exclusive)
652
653 def _mk_proxy(self, root, full_path, exclusive):
654 if self._proxy is None:
655 self._proxy = ConfigProxy(root, self, full_path, exclusive)
656 else:
657 if self._proxy.exclusive:
658 raise ValueError('Node is already owned exclusively')
659 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800660
661 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
662
663 def load_latest(self, latest_hash):
664
665 root = self._root
666 kv_store = root._kv_store
667
668 branch = ConfigBranch(self, self._auto_prune)
669 rev = PersistedConfigRevision.load(
670 branch, kv_store, self._type, latest_hash)
671 self._make_latest(branch, rev)
672 self._branches[None] = branch