blob: c6a39491c9da7c02a13c6d6bee347b008902abcc [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#
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
23from voltha.core.config.config_proxy import CallbackType, ConfigProxy
24from voltha.core.config.config_rev import is_proto_message, children_fields, \
25 ConfigRevision, access_rights
Zsolt Harasztib7067842016-11-22 18:11:53 -080026from voltha.core.config.config_rev_persisted import PersistedConfigRevision
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080027from voltha.core.config.merge_3way import merge_3way
Zsolt Harasztidafefe12016-11-14 21:29:58 -080028from voltha.protos import third_party
29from voltha.protos import meta_pb2
30
31
Zsolt Harasztidafefe12016-11-14 21:29:58 -080032def message_to_dict(m):
33 return MessageToDict(m, True, True, False)
34
35
36def check_access_violation(new_msg, old_msg):
37 """Raise ValueError if attempt is made to change a read-only field"""
38 access_map = access_rights(new_msg.__class__)
39 violated_fields = []
40 for field_name, access in access_map.iteritems():
41 if access == meta_pb2.READ_ONLY:
42 if getattr(new_msg, field_name) != getattr(old_msg, field_name):
43 violated_fields.append(field_name)
44 if violated_fields:
45 raise ValueError('Cannot change read-only field(s) %s' %
46 ', '.join('"%s"' % f for f in violated_fields))
47
48
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080049def find_rev_by_key(revs, keyname, value):
50 for i, rev in enumerate(revs):
51 if getattr(rev._config._data, keyname) == value:
52 return i, rev
53 raise KeyError('key {}={} not found'.format(keyname, value))
54
55
Zsolt Harasztidafefe12016-11-14 21:29:58 -080056class ConfigNode(object):
57 """
58 Represents a configuration node which can hold a number of revisions
59 of the configuration for this node.
60 When the configuration changes, the new version is appended to the
61 node.
62 Initial data must be a protobuf message and it will determine the type of
63 this node.
64 """
65 __slots__ = (
Zsolt Harasztib7067842016-11-22 18:11:53 -080066 '_root', # ref to root node
Zsolt Harasztidafefe12016-11-14 21:29:58 -080067 '_type', # node type, as __class__ of protobuf message
68 '_branches', # dict of transaction branches and a default (committed)
69 # branch
70 '_tags', # dict of tag-name to ref of ConfigRevision
71 '_proxy', # ref to proxy observer or None if no proxy assigned
Zsolt Harasztib7067842016-11-22 18:11:53 -080072 '_auto_prune'
Zsolt Harasztidafefe12016-11-14 21:29:58 -080073 )
74
Zsolt Harasztib7067842016-11-22 18:11:53 -080075 def __init__(self, root, initial_data, auto_prune=True, txid=None):
76 self._root = root
Zsolt Harasztidafefe12016-11-14 21:29:58 -080077 self._branches = {}
78 self._tags = {}
79 self._proxy = None
Zsolt Harasztib7067842016-11-22 18:11:53 -080080 self._auto_prune = auto_prune
Zsolt Harasztidafefe12016-11-14 21:29:58 -080081
Zsolt Harasztib7067842016-11-22 18:11:53 -080082 if isinstance(initial_data, type):
83 self._type = initial_data
84 elif is_proto_message(initial_data):
85 self._type = initial_data.__class__
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080086 copied_data = initial_data.__class__()
87 copied_data.CopyFrom(initial_data)
88 self._initialize(copied_data, txid)
Zsolt Harasztib7067842016-11-22 18:11:53 -080089 else:
90 raise NotImplementedError()
Zsolt Harasztidafefe12016-11-14 21:29:58 -080091
Zsolt Harasztib7067842016-11-22 18:11:53 -080092 def _mknode(self, *args, **kw):
93 return ConfigNode(self._root, *args, **kw)
94
95 def _mkrev(self, *args, **kw):
96 return self._root.mkrev(*args, **kw)
97
98 def _initialize(self, data, txid):
Zsolt Harasztidafefe12016-11-14 21:29:58 -080099 # separate external children data away from locally stored data
100 # based on child_node annotations in protobuf
101 children = {}
102 for field_name, field in children_fields(self._type).iteritems():
103 field_value = getattr(data, field_name)
104 if field.is_container:
105 if field.key:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800106 keys_seen = set()
107 children[field_name] = lst = []
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800108 for v in field_value:
Zsolt Harasztib7067842016-11-22 18:11:53 -0800109 rev = self._mknode(v, txid=txid).latest
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800110 key = getattr(v, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800111 if key in keys_seen:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800112 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800113 lst.append(rev)
114 keys_seen.add(key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800115 else:
116 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800117 self._mknode(v, txid=txid).latest for v in field_value]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800118 else:
119 children[field_name] = [
Zsolt Harasztib7067842016-11-22 18:11:53 -0800120 self._mknode(field_value, txid=txid).latest]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800121 data.ClearField(field_name)
122
Zsolt Harasztib7067842016-11-22 18:11:53 -0800123 branch = ConfigBranch(self, auto_prune=self._auto_prune)
124 rev = self._mkrev(branch, data, children)
125 self._make_latest(branch, rev)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800126 self._branches[txid] = branch
127
128 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ accessors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
129 # these convenience short-cuts only work for the committed branch
130
131 @property
132 def revisions(self):
133 return [r._hash for r in self._branches[None]._revs.itervalues()]
134
135 @property
136 def latest(self):
137 return self._branches[None]._latest
138
139 def __getitem__(self, hash):
140 return self._branches[None]._revs[hash]
141
142 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ get operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
143
144 def get(self, path=None, hash=None, depth=0, deep=False, txid=None):
145
146 # depth preparation
147 if deep:
148 depth = -1
149
150 # path preparation
151 path = '' if path is None else path
152 while path.startswith('/'):
153 path = path[1:]
154
155 # determine branch; if lookup fails, it is ok to use default branch
156 branch = self._branches.get(txid, None) or self._branches[None]
157
158 # determine rev
159 if hash is not None:
160 rev = branch._revs[hash]
161 else:
162 rev = branch.latest
163
164 return self._get(rev, path, depth)
165
166 def _get(self, rev, path, depth):
167
168 if not path:
169 return self._do_get(rev, depth)
170
171 # ... otherwise
172 name, _, path = path.partition('/')
173 field = children_fields(self._type)[name]
174 if field.is_container:
175 if field.key:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800176 children = rev._children[name]
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800177 if path:
178 # need to escalate further
179 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800180 key = field.key_from_str(key)
181 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800182 child_node = child_rev.node
183 return child_node._get(child_rev, path, depth)
184 else:
185 # we are the node of interest
186 response = []
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800187 for child_rev in children:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800188 child_node = child_rev.node
189 value = child_node._do_get(child_rev, depth)
190 response.append(value)
191 return response
192 else:
193 if path:
194 raise LookupError(
195 'Cannot index into container with no key defined')
196 response = []
197 for child_rev in rev._children[name]:
198 child_node = child_rev.node
199 value = child_node._do_get(child_rev, depth)
200 response.append(value)
201 return response
202 else:
203 child_rev = rev._children[name][0]
204 child_node = child_rev.node
205 return child_node._get(child_rev, path, depth)
206
207 def _do_get(self, rev, depth):
208 msg = rev.get(depth)
209 if self._proxy is not None:
210 msg = self._proxy.invoke_callbacks(CallbackType.GET, msg)
211 return msg
212
213 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ update operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
214
215 def update(self, path, data, strict=False, txid=None, mk_branch=None):
216
217 while path.startswith('/'):
218 path = path[1:]
219
220 try:
221 branch = self._branches[txid]
222 except KeyError:
223 branch = mk_branch(self)
224
225 if not path:
226 return self._do_update(branch, data, strict)
227
228 rev = branch._latest # change is always made to the latest
229 name, _, path = path.partition('/')
230 field = children_fields(self._type)[name]
231 if field.is_container:
232 if not path:
233 raise ValueError('Cannot update a list')
234 if field.key:
235 key, _, path = path.partition('/')
236 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800237 children = copy(rev._children[name])
238 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800239 child_node = child_rev.node
240 new_child_rev = child_node.update(
241 path, data, strict, txid, mk_branch)
242 if new_child_rev.hash == child_rev.hash:
243 # no change, we can return
244 return branch._latest
245 if getattr(new_child_rev.data, field.key) != key:
246 raise ValueError('Cannot change key field')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800247 children[idx] = new_child_rev
248 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800249 self._make_latest(branch, rev)
250 return rev
251 else:
252 raise ValueError('Cannot index into container with no keys')
253
254 else:
255 child_rev = rev._children[name][0]
256 child_node = child_rev.node
257 new_child_rev = child_node.update(
258 path, data, strict, txid, mk_branch)
259 rev = rev.update_children(name, [new_child_rev], branch)
260 self._make_latest(branch, rev)
261 return rev
262
263 def _do_update(self, branch, data, strict):
264 if not isinstance(data, self._type):
265 raise ValueError(
266 '"{}" is not a valid data type for this node'.format(
267 data.__class__.__name__))
268 self._test_no_children(data)
269 if self._proxy is not None:
270 self._proxy.invoke_callbacks(CallbackType.PRE_UPDATE, data)
271
272 if branch._latest.data != data:
273 if strict:
274 # check if attempt is made to change read-only field
275 check_access_violation(data, branch._latest.data)
276 rev = branch._latest.update_data(data, branch)
277 self._make_latest(branch, rev,
278 ((CallbackType.POST_UPDATE, rev.data),))
279 return rev
280 else:
281 return branch._latest
282
283 def _make_latest(self, branch, rev, change_announcements=()):
284 branch._latest = rev
285 if rev.hash not in branch._revs:
286 branch._revs[rev.hash] = rev
287
288 # announce only if this is main branch
289 if change_announcements and branch._txid is None and \
290 self._proxy is not None:
291 for change_type, data in change_announcements:
Zsolt Haraszti66862032016-11-28 14:28:39 -0800292 # since the callback may operate on the config tree,
293 # we have to defer the execution of the callbacks till
294 # the change is propagated to the root, then root will
295 # call the callbacks
296 self._root.enqueue_callback(
297 self._proxy.invoke_callbacks,
298 change_type,
299 data,
300 proceed_on_errors=1
301 )
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800302
303 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
304
305 def add(self, path, data, txid=None, mk_branch=None):
306 while path.startswith('/'):
307 path = path[1:]
308 if not path:
309 raise ValueError('Cannot add to non-container node')
310
311 try:
312 branch = self._branches[txid]
313 except KeyError:
314 branch = mk_branch(self)
315
316 rev = branch._latest # change is always made to latest
317 name, _, path = path.partition('/')
318 field = children_fields(self._type)[name]
319 if field.is_container:
320 if not path:
321 # we do need to add a new child to the field
322 if field.key:
323 if self._proxy is not None:
324 self._proxy.invoke_callbacks(
325 CallbackType.PRE_ADD, data)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800326 children = copy(rev._children[name])
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800327 key = getattr(data, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800328 try:
329 find_rev_by_key(children, field.key, key)
330 except KeyError:
331 pass
332 else:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800333 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800334 child_rev = self._mknode(data).latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800335 children.append(child_rev)
336 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800337 self._make_latest(branch, rev,
338 ((CallbackType.POST_ADD, data),))
339 return rev
340 else:
341 # adding to non-keyed containers not implemented yet
342 raise ValueError('Cannot add to non-keyed container')
343 else:
344 if field.key:
345 # need to escalate
346 key, _, path = path.partition('/')
347 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800348 children = copy(rev._children[name])
349 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800350 child_node = child_rev.node
351 new_child_rev = child_node.add(path, data, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800352 children[idx] = new_child_rev
353 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800354 self._make_latest(branch, rev)
355 return rev
356 else:
357 raise ValueError(
358 'Cannot index into container with no keys')
359 else:
360 raise ValueError('Cannot add to non-container field')
361
362 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
363
364 def remove(self, path, txid=None, mk_branch=None):
365 while path.startswith('/'):
366 path = path[1:]
367 if not path:
368 raise ValueError('Cannot remove from non-container node')
369
370 try:
371 branch = self._branches[txid]
372 except KeyError:
373 branch = mk_branch(self)
374
375 rev = branch._latest # change is always made to latest
376 name, _, path = path.partition('/')
377 field = children_fields(self._type)[name]
378 if field.is_container:
379 if not path:
380 raise ValueError("Cannot remove without a key")
381 if field.key:
382 key, _, path = path.partition('/')
383 key = field.key_from_str(key)
384 if path:
385 # need to escalate
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800386 children = copy(rev._children[name])
387 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800388 child_node = child_rev.node
389 new_child_rev = child_node.remove(path, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800390 children[idx] = new_child_rev
391 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800392 self._make_latest(branch, rev)
393 return rev
394 else:
395 # need to remove from this very node
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800396 children = copy(rev._children[name])
397 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800398 if self._proxy is not None:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800399 data = child_rev.data
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800400 self._proxy.invoke_callbacks(
401 CallbackType.PRE_REMOVE, data)
402 post_anno = ((CallbackType.POST_REMOVE, data),)
403 else:
404 post_anno = ()
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800405 del children[idx]
406 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800407 self._make_latest(branch, rev, post_anno)
408 return rev
409 else:
410 raise ValueError('Cannot remove from non-keyed container')
411 else:
412 raise ValueError('Cannot remove non-conatiner field')
413
414 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
415
416 def _mk_txbranch(self, txid):
417 branch_point = self._branches[None].latest
418 branch = ConfigBranch(self, txid, branch_point)
419 self._branches[txid] = branch
420 return branch
421
422 def _del_txbranch(self, txid):
423 del self._branches[txid]
424
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800425 def _merge_txbranch(self, txid, dry_run=False):
426 """
427 Make latest in branch to be latest in the common branch, but only
428 if no conflict is detected. Conflict is where the txbranch branch
429 point no longer matches the latest in the default branch. This has
430 to be verified recursively.
431 """
432
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800433 def merge_child(child_rev):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800434 child_branch = child_rev._branch
435 if child_branch._txid == txid:
436 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
437 return child_rev
438
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800439 src_branch = self._branches[txid]
440 dst_branch = self._branches[None]
441
442 fork_rev = src_branch.origin # rev from which src branch was made
443 src_rev = src_branch.latest # head rev of source branch
444 dst_rev = dst_branch.latest # head rev of target branch
445
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800446 rev, changes = merge_3way(
447 fork_rev, src_rev, dst_rev, merge_child, dry_run)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800448
449 if not dry_run:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800450 self._make_latest(dst_branch, rev, change_announcements=changes)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800451 del self._branches[txid]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800452
453 return rev
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800454
455 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
456
457 def diff(self, hash1, hash2=None, txid=None):
458 branch = self._branches[txid]
459 rev1 = branch[hash1]
460 rev2 = branch[hash2] if hash2 else branch._latest
461 if rev1.hash == rev2.hash:
462 return JsonPatch([])
463 else:
464 dict1 = message_to_dict(rev1.data)
465 dict2 = message_to_dict(rev2.data)
466 return make_patch(dict1, dict2)
467
468 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
469
470 def tag(self, tag, hash=None):
471 branch = self._branches[None] # tag only what has been committed
472 rev = branch._latest if hash is None else branch._revs[hash]
473 self._tags[tag] = rev
474 return self
475
476 @property
477 def tags(self):
478 return sorted(self._tags.iterkeys())
479
480 def by_tag(self, tag):
481 """
482 Return revision based on tag
483 :param tag: previously registered tag value
484 :return: revision object
485 """
486 return self._tags[tag]
487
488 def diff_by_tag(self, tag1, tag2):
489 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
490
491 def delete_tag(self, tag):
492 del self._tags[tag]
493
494 def delete_tags(self, *tags):
495 for tag in tags:
496 del self._tags[tag]
497
498 def prune_untagged(self):
499 branch = self._branches[None]
500 keep = set(rev.hash for rev in self._tags.itervalues())
501 keep.add(branch._latest.hash)
502 for hash in branch._revs.keys():
503 if hash not in keep:
504 del branch._revs[hash]
505 return self
506
507 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
508
509 def _test_no_children(self, data):
510 for field_name, field in children_fields(self._type).items():
511 field_value = getattr(data, field_name)
512 if field.is_container:
513 if len(field_value):
514 raise NotImplementedError(
515 'Cannot update external children')
516 else:
517 if data.HasField(field_name):
518 raise NotImplementedError(
519 'Cannot update externel children')
520
521 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
522
523 def get_proxy(self, path, exclusive=False):
524 return self._get_proxy(path, self, path, exclusive)
525
526 def _get_proxy(self, path, root, full_path, exclusive):
527 while path.startswith('/'):
528 path = path[1:]
529 if not path:
530 return self._mk_proxy(root, full_path, exclusive)
531
532 # need to escalate
533 rev = self._branches[None]._latest
534 name, _, path = path.partition('/')
535 field = children_fields(self._type)[name]
536 if field.is_container:
537 if not path:
538 raise ValueError('Cannot proxy a container field')
539 if field.key:
540 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800541 key = field.key_from_str(key)
542 children = rev._children[name]
543 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800544 child_node = child_rev.node
545 return child_node._get_proxy(path, root, full_path, exclusive)
546
547 raise ValueError('Cannot index into container with no keys')
548
549 else:
550 child_rev = rev._children[name][0]
551 child_node = child_rev.node
552 return child_node._get_proxy(path, root, full_path, exclusive)
553
554 def _mk_proxy(self, root, full_path, exclusive):
555 if self._proxy is None:
556 self._proxy = ConfigProxy(root, self, full_path, exclusive)
557 else:
558 if self._proxy.exclusive:
559 raise ValueError('Node is already owned exclusively')
560 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800561
562 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
563
564 def load_latest(self, latest_hash):
565
566 root = self._root
567 kv_store = root._kv_store
568
569 branch = ConfigBranch(self, self._auto_prune)
570 rev = PersistedConfigRevision.load(
571 branch, kv_store, self._type, latest_hash)
572 self._make_latest(branch, rev)
573 self._branches[None] = branch