blob: cba6d887e7a05cb703f3597f917db00431576ecb [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
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
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800474 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800475 return self
476
477 @property
478 def tags(self):
479 return sorted(self._tags.iterkeys())
480
481 def by_tag(self, tag):
482 """
483 Return revision based on tag
484 :param tag: previously registered tag value
485 :return: revision object
486 """
487 return self._tags[tag]
488
489 def diff_by_tag(self, tag1, tag2):
490 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
491
492 def delete_tag(self, tag):
493 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800494 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800495
496 def delete_tags(self, *tags):
497 for tag in tags:
498 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800499 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800500
501 def prune_untagged(self):
502 branch = self._branches[None]
503 keep = set(rev.hash for rev in self._tags.itervalues())
504 keep.add(branch._latest.hash)
505 for hash in branch._revs.keys():
506 if hash not in keep:
507 del branch._revs[hash]
508 return self
509
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800510 def persist_tags(self):
511 """
512 Persist tag information to the backend
513 """
514
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800515 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
516
517 def _test_no_children(self, data):
518 for field_name, field in children_fields(self._type).items():
519 field_value = getattr(data, field_name)
520 if field.is_container:
521 if len(field_value):
522 raise NotImplementedError(
523 'Cannot update external children')
524 else:
525 if data.HasField(field_name):
526 raise NotImplementedError(
527 'Cannot update externel children')
528
529 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
530
531 def get_proxy(self, path, exclusive=False):
532 return self._get_proxy(path, self, path, exclusive)
533
534 def _get_proxy(self, path, root, full_path, exclusive):
535 while path.startswith('/'):
536 path = path[1:]
537 if not path:
538 return self._mk_proxy(root, full_path, exclusive)
539
540 # need to escalate
541 rev = self._branches[None]._latest
542 name, _, path = path.partition('/')
543 field = children_fields(self._type)[name]
544 if field.is_container:
545 if not path:
546 raise ValueError('Cannot proxy a container field')
547 if field.key:
548 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800549 key = field.key_from_str(key)
550 children = rev._children[name]
551 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800552 child_node = child_rev.node
553 return child_node._get_proxy(path, root, full_path, exclusive)
554
555 raise ValueError('Cannot index into container with no keys')
556
557 else:
558 child_rev = rev._children[name][0]
559 child_node = child_rev.node
560 return child_node._get_proxy(path, root, full_path, exclusive)
561
562 def _mk_proxy(self, root, full_path, exclusive):
563 if self._proxy is None:
564 self._proxy = ConfigProxy(root, self, full_path, exclusive)
565 else:
566 if self._proxy.exclusive:
567 raise ValueError('Node is already owned exclusively')
568 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800569
570 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
571
572 def load_latest(self, latest_hash):
573
574 root = self._root
575 kv_store = root._kv_store
576
577 branch = ConfigBranch(self, self._auto_prune)
578 rev = PersistedConfigRevision.load(
579 branch, kv_store, self._type, latest_hash)
580 self._make_latest(branch, rev)
581 self._branches[None] = branch