blob: f1c8fc03dbaa68150d96f81d895a11e6acf1bf32 [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
Khen Nursimulu56b36472017-03-08 15:32:42 -0500307 for change_type, data in change_announcements:
308 self._root.enqueue_notification_callback(
309 self._mk_event_bus().advertise,
310 change_type,
311 data,
Khen Nursimuluc60afa12017-03-13 14:33:50 -0400312 hash=rev.hash
Khen Nursimulu56b36472017-03-08 15:32:42 -0500313 )
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800314
315 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
316
317 def add(self, path, data, txid=None, mk_branch=None):
318 while path.startswith('/'):
319 path = path[1:]
320 if not path:
321 raise ValueError('Cannot add to non-container node')
322
323 try:
324 branch = self._branches[txid]
325 except KeyError:
326 branch = mk_branch(self)
327
328 rev = branch._latest # change is always made to latest
329 name, _, path = path.partition('/')
330 field = children_fields(self._type)[name]
331 if field.is_container:
332 if not path:
333 # we do need to add a new child to the field
334 if field.key:
335 if self._proxy is not None:
336 self._proxy.invoke_callbacks(
337 CallbackType.PRE_ADD, data)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800338 children = copy(rev._children[name])
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800339 key = getattr(data, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800340 try:
341 find_rev_by_key(children, field.key, key)
342 except KeyError:
343 pass
344 else:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800345 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800346 child_rev = self._mknode(data).latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800347 children.append(child_rev)
348 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800349 self._make_latest(branch, rev,
350 ((CallbackType.POST_ADD, data),))
351 return rev
352 else:
353 # adding to non-keyed containers not implemented yet
354 raise ValueError('Cannot add to non-keyed container')
355 else:
356 if field.key:
357 # need to escalate
358 key, _, path = path.partition('/')
359 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800360 children = copy(rev._children[name])
361 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800362 child_node = child_rev.node
363 new_child_rev = child_node.add(path, data, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800364 children[idx] = new_child_rev
365 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800366 self._make_latest(branch, rev)
367 return rev
368 else:
369 raise ValueError(
370 'Cannot index into container with no keys')
371 else:
372 raise ValueError('Cannot add to non-container field')
373
374 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
375
376 def remove(self, path, txid=None, mk_branch=None):
377 while path.startswith('/'):
378 path = path[1:]
379 if not path:
380 raise ValueError('Cannot remove from non-container node')
381
382 try:
383 branch = self._branches[txid]
384 except KeyError:
385 branch = mk_branch(self)
386
387 rev = branch._latest # change is always made to latest
388 name, _, path = path.partition('/')
389 field = children_fields(self._type)[name]
390 if field.is_container:
391 if not path:
392 raise ValueError("Cannot remove without a key")
393 if field.key:
394 key, _, path = path.partition('/')
395 key = field.key_from_str(key)
396 if path:
397 # need to escalate
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800398 children = copy(rev._children[name])
399 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800400 child_node = child_rev.node
401 new_child_rev = child_node.remove(path, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800402 children[idx] = new_child_rev
403 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800404 self._make_latest(branch, rev)
405 return rev
406 else:
407 # need to remove from this very node
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800408 children = copy(rev._children[name])
409 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800410 if self._proxy is not None:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800411 data = child_rev.data
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800412 self._proxy.invoke_callbacks(
413 CallbackType.PRE_REMOVE, data)
414 post_anno = ((CallbackType.POST_REMOVE, data),)
415 else:
alshabibfa007ce2017-02-22 22:19:51 -0800416 post_anno = ((CallbackType.POST_REMOVE, child_rev.data),)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800417 del children[idx]
418 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800419 self._make_latest(branch, rev, post_anno)
420 return rev
421 else:
422 raise ValueError('Cannot remove from non-keyed container')
423 else:
424 raise ValueError('Cannot remove non-conatiner field')
425
426 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
427
428 def _mk_txbranch(self, txid):
429 branch_point = self._branches[None].latest
430 branch = ConfigBranch(self, txid, branch_point)
431 self._branches[txid] = branch
432 return branch
433
434 def _del_txbranch(self, txid):
435 del self._branches[txid]
436
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800437 def _merge_txbranch(self, txid, dry_run=False):
438 """
439 Make latest in branch to be latest in the common branch, but only
440 if no conflict is detected. Conflict is where the txbranch branch
441 point no longer matches the latest in the default branch. This has
442 to be verified recursively.
443 """
444
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800445 def merge_child(child_rev):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800446 child_branch = child_rev._branch
447 if child_branch._txid == txid:
448 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
449 return child_rev
450
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800451 src_branch = self._branches[txid]
452 dst_branch = self._branches[None]
453
454 fork_rev = src_branch.origin # rev from which src branch was made
455 src_rev = src_branch.latest # head rev of source branch
456 dst_rev = dst_branch.latest # head rev of target branch
457
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800458 rev, changes = merge_3way(
459 fork_rev, src_rev, dst_rev, merge_child, dry_run)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800460
461 if not dry_run:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800462 self._make_latest(dst_branch, rev, change_announcements=changes)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800463 del self._branches[txid]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800464
465 return rev
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800466
467 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
468
469 def diff(self, hash1, hash2=None, txid=None):
470 branch = self._branches[txid]
471 rev1 = branch[hash1]
472 rev2 = branch[hash2] if hash2 else branch._latest
473 if rev1.hash == rev2.hash:
474 return JsonPatch([])
475 else:
476 dict1 = message_to_dict(rev1.data)
477 dict2 = message_to_dict(rev2.data)
478 return make_patch(dict1, dict2)
479
480 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
481
482 def tag(self, tag, hash=None):
483 branch = self._branches[None] # tag only what has been committed
484 rev = branch._latest if hash is None else branch._revs[hash]
485 self._tags[tag] = rev
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800486 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800487 return self
488
489 @property
490 def tags(self):
491 return sorted(self._tags.iterkeys())
492
493 def by_tag(self, tag):
494 """
495 Return revision based on tag
496 :param tag: previously registered tag value
497 :return: revision object
498 """
499 return self._tags[tag]
500
501 def diff_by_tag(self, tag1, tag2):
502 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
503
504 def delete_tag(self, tag):
505 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800506 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800507
508 def delete_tags(self, *tags):
509 for tag in tags:
510 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800511 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800512
513 def prune_untagged(self):
514 branch = self._branches[None]
515 keep = set(rev.hash for rev in self._tags.itervalues())
516 keep.add(branch._latest.hash)
517 for hash in branch._revs.keys():
518 if hash not in keep:
519 del branch._revs[hash]
520 return self
521
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800522 def persist_tags(self):
523 """
524 Persist tag information to the backend
525 """
526
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800527 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
528
529 def _test_no_children(self, data):
530 for field_name, field in children_fields(self._type).items():
531 field_value = getattr(data, field_name)
532 if field.is_container:
533 if len(field_value):
534 raise NotImplementedError(
535 'Cannot update external children')
536 else:
537 if data.HasField(field_name):
538 raise NotImplementedError(
539 'Cannot update externel children')
540
541 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
542
543 def get_proxy(self, path, exclusive=False):
544 return self._get_proxy(path, self, path, exclusive)
545
546 def _get_proxy(self, path, root, full_path, exclusive):
547 while path.startswith('/'):
548 path = path[1:]
549 if not path:
550 return self._mk_proxy(root, full_path, exclusive)
551
552 # need to escalate
553 rev = self._branches[None]._latest
554 name, _, path = path.partition('/')
555 field = children_fields(self._type)[name]
556 if field.is_container:
557 if not path:
558 raise ValueError('Cannot proxy a container field')
559 if field.key:
560 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800561 key = field.key_from_str(key)
562 children = rev._children[name]
563 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800564 child_node = child_rev.node
565 return child_node._get_proxy(path, root, full_path, exclusive)
566
567 raise ValueError('Cannot index into container with no keys')
568
569 else:
570 child_rev = rev._children[name][0]
571 child_node = child_rev.node
572 return child_node._get_proxy(path, root, full_path, exclusive)
573
574 def _mk_proxy(self, root, full_path, exclusive):
575 if self._proxy is None:
576 self._proxy = ConfigProxy(root, self, full_path, exclusive)
577 else:
578 if self._proxy.exclusive:
579 raise ValueError('Node is already owned exclusively')
580 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800581
alshabibfa007ce2017-02-22 22:19:51 -0800582 def _mk_event_bus(self):
583 if self._event_bus is None:
584 self._event_bus = ConfigEventBus()
585 return self._event_bus
586
Zsolt Harasztib7067842016-11-22 18:11:53 -0800587 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
588
589 def load_latest(self, latest_hash):
590
591 root = self._root
592 kv_store = root._kv_store
593
594 branch = ConfigBranch(self, self._auto_prune)
595 rev = PersistedConfigRevision.load(
596 branch, kv_store, self._type, latest_hash)
597 self._make_latest(branch, rev)
598 self._branches[None] = branch