blob: 969ba8e2aed6d466faae293f05ab8a794563aacd [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:
292 self._proxy.invoke_callbacks(
293 change_type, data, proceed_on_errors=1)
294
295 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
296
297 def add(self, path, data, txid=None, mk_branch=None):
298 while path.startswith('/'):
299 path = path[1:]
300 if not path:
301 raise ValueError('Cannot add to non-container node')
302
303 try:
304 branch = self._branches[txid]
305 except KeyError:
306 branch = mk_branch(self)
307
308 rev = branch._latest # change is always made to latest
309 name, _, path = path.partition('/')
310 field = children_fields(self._type)[name]
311 if field.is_container:
312 if not path:
313 # we do need to add a new child to the field
314 if field.key:
315 if self._proxy is not None:
316 self._proxy.invoke_callbacks(
317 CallbackType.PRE_ADD, data)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800318 children = copy(rev._children[name])
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800319 key = getattr(data, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800320 try:
321 find_rev_by_key(children, field.key, key)
322 except KeyError:
323 pass
324 else:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800325 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800326 child_rev = self._mknode(data).latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800327 children.append(child_rev)
328 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800329 self._make_latest(branch, rev,
330 ((CallbackType.POST_ADD, data),))
331 return rev
332 else:
333 # adding to non-keyed containers not implemented yet
334 raise ValueError('Cannot add to non-keyed container')
335 else:
336 if field.key:
337 # need to escalate
338 key, _, path = path.partition('/')
339 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800340 children = copy(rev._children[name])
341 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800342 child_node = child_rev.node
343 new_child_rev = child_node.add(path, data, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800344 children[idx] = new_child_rev
345 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800346 self._make_latest(branch, rev)
347 return rev
348 else:
349 raise ValueError(
350 'Cannot index into container with no keys')
351 else:
352 raise ValueError('Cannot add to non-container field')
353
354 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
355
356 def remove(self, path, txid=None, mk_branch=None):
357 while path.startswith('/'):
358 path = path[1:]
359 if not path:
360 raise ValueError('Cannot remove from non-container node')
361
362 try:
363 branch = self._branches[txid]
364 except KeyError:
365 branch = mk_branch(self)
366
367 rev = branch._latest # change is always made to latest
368 name, _, path = path.partition('/')
369 field = children_fields(self._type)[name]
370 if field.is_container:
371 if not path:
372 raise ValueError("Cannot remove without a key")
373 if field.key:
374 key, _, path = path.partition('/')
375 key = field.key_from_str(key)
376 if path:
377 # need to escalate
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800378 children = copy(rev._children[name])
379 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800380 child_node = child_rev.node
381 new_child_rev = child_node.remove(path, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800382 children[idx] = new_child_rev
383 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800384 self._make_latest(branch, rev)
385 return rev
386 else:
387 # need to remove from this very node
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800388 children = copy(rev._children[name])
389 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800390 if self._proxy is not None:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800391 data = child_rev.data
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800392 self._proxy.invoke_callbacks(
393 CallbackType.PRE_REMOVE, data)
394 post_anno = ((CallbackType.POST_REMOVE, data),)
395 else:
396 post_anno = ()
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800397 del children[idx]
398 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800399 self._make_latest(branch, rev, post_anno)
400 return rev
401 else:
402 raise ValueError('Cannot remove from non-keyed container')
403 else:
404 raise ValueError('Cannot remove non-conatiner field')
405
406 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
407
408 def _mk_txbranch(self, txid):
409 branch_point = self._branches[None].latest
410 branch = ConfigBranch(self, txid, branch_point)
411 self._branches[txid] = branch
412 return branch
413
414 def _del_txbranch(self, txid):
415 del self._branches[txid]
416
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800417 def _merge_txbranch(self, txid, dry_run=False):
418 """
419 Make latest in branch to be latest in the common branch, but only
420 if no conflict is detected. Conflict is where the txbranch branch
421 point no longer matches the latest in the default branch. This has
422 to be verified recursively.
423 """
424
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800425 def merge_child(child_rev):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800426 child_branch = child_rev._branch
427 if child_branch._txid == txid:
428 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
429 return child_rev
430
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800431 src_branch = self._branches[txid]
432 dst_branch = self._branches[None]
433
434 fork_rev = src_branch.origin # rev from which src branch was made
435 src_rev = src_branch.latest # head rev of source branch
436 dst_rev = dst_branch.latest # head rev of target branch
437
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800438 rev, changes = merge_3way(
439 fork_rev, src_rev, dst_rev, merge_child, dry_run)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800440
441 if not dry_run:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800442 self._make_latest(dst_branch, rev, change_announcements=changes)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800443 del self._branches[txid]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800444
445 return rev
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800446
447 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
448
449 def diff(self, hash1, hash2=None, txid=None):
450 branch = self._branches[txid]
451 rev1 = branch[hash1]
452 rev2 = branch[hash2] if hash2 else branch._latest
453 if rev1.hash == rev2.hash:
454 return JsonPatch([])
455 else:
456 dict1 = message_to_dict(rev1.data)
457 dict2 = message_to_dict(rev2.data)
458 return make_patch(dict1, dict2)
459
460 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
461
462 def tag(self, tag, hash=None):
463 branch = self._branches[None] # tag only what has been committed
464 rev = branch._latest if hash is None else branch._revs[hash]
465 self._tags[tag] = rev
466 return self
467
468 @property
469 def tags(self):
470 return sorted(self._tags.iterkeys())
471
472 def by_tag(self, tag):
473 """
474 Return revision based on tag
475 :param tag: previously registered tag value
476 :return: revision object
477 """
478 return self._tags[tag]
479
480 def diff_by_tag(self, tag1, tag2):
481 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
482
483 def delete_tag(self, tag):
484 del self._tags[tag]
485
486 def delete_tags(self, *tags):
487 for tag in tags:
488 del self._tags[tag]
489
490 def prune_untagged(self):
491 branch = self._branches[None]
492 keep = set(rev.hash for rev in self._tags.itervalues())
493 keep.add(branch._latest.hash)
494 for hash in branch._revs.keys():
495 if hash not in keep:
496 del branch._revs[hash]
497 return self
498
499 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
500
501 def _test_no_children(self, data):
502 for field_name, field in children_fields(self._type).items():
503 field_value = getattr(data, field_name)
504 if field.is_container:
505 if len(field_value):
506 raise NotImplementedError(
507 'Cannot update external children')
508 else:
509 if data.HasField(field_name):
510 raise NotImplementedError(
511 'Cannot update externel children')
512
513 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
514
515 def get_proxy(self, path, exclusive=False):
516 return self._get_proxy(path, self, path, exclusive)
517
518 def _get_proxy(self, path, root, full_path, exclusive):
519 while path.startswith('/'):
520 path = path[1:]
521 if not path:
522 return self._mk_proxy(root, full_path, exclusive)
523
524 # need to escalate
525 rev = self._branches[None]._latest
526 name, _, path = path.partition('/')
527 field = children_fields(self._type)[name]
528 if field.is_container:
529 if not path:
530 raise ValueError('Cannot proxy a container field')
531 if field.key:
532 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800533 key = field.key_from_str(key)
534 children = rev._children[name]
535 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800536 child_node = child_rev.node
537 return child_node._get_proxy(path, root, full_path, exclusive)
538
539 raise ValueError('Cannot index into container with no keys')
540
541 else:
542 child_rev = rev._children[name][0]
543 child_node = child_rev.node
544 return child_node._get_proxy(path, root, full_path, exclusive)
545
546 def _mk_proxy(self, root, full_path, exclusive):
547 if self._proxy is None:
548 self._proxy = ConfigProxy(root, self, full_path, exclusive)
549 else:
550 if self._proxy.exclusive:
551 raise ValueError('Node is already owned exclusively')
552 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800553
554 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
555
556 def load_latest(self, latest_hash):
557
558 root = self._root
559 kv_store = root._kv_store
560
561 branch = ConfigBranch(self, self._auto_prune)
562 rev = PersistedConfigRevision.load(
563 branch, kv_store, self._type, latest_hash)
564 self._make_latest(branch, rev)
565 self._branches[None] = branch