blob: f679786c40993dddc2a685d5e1acc8717581e3ed [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
307
Khen Nursimulu29e75502017-03-07 17:26:50 -0500308 # TODO: This fix needs to be investigated/reworked as it causes an
309 # extra item to be queued in the main branch. This item does
310 # not get consumed, hence preventing any device update on the
311 # main branch
312 # for change_type, data in change_announcements:
313 # self._root.enqueue_callback(
314 # self._mk_event_bus().advertise,
315 # change_type,
316 # data,
317 # hash=rev.hash
318 # )
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800319
320 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ add operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
321
322 def add(self, path, data, txid=None, mk_branch=None):
323 while path.startswith('/'):
324 path = path[1:]
325 if not path:
326 raise ValueError('Cannot add to non-container node')
327
328 try:
329 branch = self._branches[txid]
330 except KeyError:
331 branch = mk_branch(self)
332
333 rev = branch._latest # change is always made to latest
334 name, _, path = path.partition('/')
335 field = children_fields(self._type)[name]
336 if field.is_container:
337 if not path:
338 # we do need to add a new child to the field
339 if field.key:
340 if self._proxy is not None:
341 self._proxy.invoke_callbacks(
342 CallbackType.PRE_ADD, data)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800343 children = copy(rev._children[name])
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800344 key = getattr(data, field.key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800345 try:
346 find_rev_by_key(children, field.key, key)
347 except KeyError:
348 pass
349 else:
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800350 raise ValueError('Duplicate key "{}"'.format(key))
Zsolt Harasztib7067842016-11-22 18:11:53 -0800351 child_rev = self._mknode(data).latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800352 children.append(child_rev)
353 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800354 self._make_latest(branch, rev,
355 ((CallbackType.POST_ADD, data),))
356 return rev
357 else:
358 # adding to non-keyed containers not implemented yet
359 raise ValueError('Cannot add to non-keyed container')
360 else:
361 if field.key:
362 # need to escalate
363 key, _, path = path.partition('/')
364 key = field.key_from_str(key)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800365 children = copy(rev._children[name])
366 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800367 child_node = child_rev.node
368 new_child_rev = child_node.add(path, data, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800369 children[idx] = new_child_rev
370 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800371 self._make_latest(branch, rev)
372 return rev
373 else:
374 raise ValueError(
375 'Cannot index into container with no keys')
376 else:
377 raise ValueError('Cannot add to non-container field')
378
379 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ remove operation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
380
381 def remove(self, path, txid=None, mk_branch=None):
382 while path.startswith('/'):
383 path = path[1:]
384 if not path:
385 raise ValueError('Cannot remove from non-container node')
386
387 try:
388 branch = self._branches[txid]
389 except KeyError:
390 branch = mk_branch(self)
391
392 rev = branch._latest # change is always made to latest
393 name, _, path = path.partition('/')
394 field = children_fields(self._type)[name]
395 if field.is_container:
396 if not path:
397 raise ValueError("Cannot remove without a key")
398 if field.key:
399 key, _, path = path.partition('/')
400 key = field.key_from_str(key)
401 if path:
402 # need to escalate
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800403 children = copy(rev._children[name])
404 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800405 child_node = child_rev.node
406 new_child_rev = child_node.remove(path, txid, mk_branch)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800407 children[idx] = new_child_rev
408 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800409 self._make_latest(branch, rev)
410 return rev
411 else:
412 # need to remove from this very node
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800413 children = copy(rev._children[name])
414 idx, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800415 if self._proxy is not None:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800416 data = child_rev.data
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800417 self._proxy.invoke_callbacks(
418 CallbackType.PRE_REMOVE, data)
419 post_anno = ((CallbackType.POST_REMOVE, data),)
420 else:
alshabibfa007ce2017-02-22 22:19:51 -0800421 post_anno = ((CallbackType.POST_REMOVE, child_rev.data),)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800422 del children[idx]
423 rev = rev.update_children(name, children, branch)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800424 self._make_latest(branch, rev, post_anno)
425 return rev
426 else:
427 raise ValueError('Cannot remove from non-keyed container')
428 else:
429 raise ValueError('Cannot remove non-conatiner field')
430
431 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Branching ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
432
433 def _mk_txbranch(self, txid):
434 branch_point = self._branches[None].latest
435 branch = ConfigBranch(self, txid, branch_point)
436 self._branches[txid] = branch
437 return branch
438
439 def _del_txbranch(self, txid):
440 del self._branches[txid]
441
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800442 def _merge_txbranch(self, txid, dry_run=False):
443 """
444 Make latest in branch to be latest in the common branch, but only
445 if no conflict is detected. Conflict is where the txbranch branch
446 point no longer matches the latest in the default branch. This has
447 to be verified recursively.
448 """
449
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800450 def merge_child(child_rev):
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800451 child_branch = child_rev._branch
452 if child_branch._txid == txid:
453 child_rev = child_branch._node._merge_txbranch(txid, dry_run)
454 return child_rev
455
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800456 src_branch = self._branches[txid]
457 dst_branch = self._branches[None]
458
459 fork_rev = src_branch.origin # rev from which src branch was made
460 src_rev = src_branch.latest # head rev of source branch
461 dst_rev = dst_branch.latest # head rev of target branch
462
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800463 rev, changes = merge_3way(
464 fork_rev, src_rev, dst_rev, merge_child, dry_run)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800465
466 if not dry_run:
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800467 self._make_latest(dst_branch, rev, change_announcements=changes)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800468 del self._branches[txid]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800469
470 return rev
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800471
472 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Diff utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
473
474 def diff(self, hash1, hash2=None, txid=None):
475 branch = self._branches[txid]
476 rev1 = branch[hash1]
477 rev2 = branch[hash2] if hash2 else branch._latest
478 if rev1.hash == rev2.hash:
479 return JsonPatch([])
480 else:
481 dict1 = message_to_dict(rev1.data)
482 dict2 = message_to_dict(rev2.data)
483 return make_patch(dict1, dict2)
484
485 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tagging utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
486
487 def tag(self, tag, hash=None):
488 branch = self._branches[None] # tag only what has been committed
489 rev = branch._latest if hash is None else branch._revs[hash]
490 self._tags[tag] = rev
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800491 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800492 return self
493
494 @property
495 def tags(self):
496 return sorted(self._tags.iterkeys())
497
498 def by_tag(self, tag):
499 """
500 Return revision based on tag
501 :param tag: previously registered tag value
502 :return: revision object
503 """
504 return self._tags[tag]
505
506 def diff_by_tag(self, tag1, tag2):
507 return self.diff(self._tags[tag1].hash, self._tags[tag2].hash)
508
509 def delete_tag(self, tag):
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 delete_tags(self, *tags):
514 for tag in tags:
515 del self._tags[tag]
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800516 self.persist_tags()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800517
518 def prune_untagged(self):
519 branch = self._branches[None]
520 keep = set(rev.hash for rev in self._tags.itervalues())
521 keep.add(branch._latest.hash)
522 for hash in branch._revs.keys():
523 if hash not in keep:
524 del branch._revs[hash]
525 return self
526
Ryan Van Gilderbf974d02017-02-24 15:01:22 -0800527 def persist_tags(self):
528 """
529 Persist tag information to the backend
530 """
531
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800532 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
533
534 def _test_no_children(self, data):
535 for field_name, field in children_fields(self._type).items():
536 field_value = getattr(data, field_name)
537 if field.is_container:
538 if len(field_value):
539 raise NotImplementedError(
540 'Cannot update external children')
541 else:
542 if data.HasField(field_name):
543 raise NotImplementedError(
544 'Cannot update externel children')
545
546 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Node proxy ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
547
548 def get_proxy(self, path, exclusive=False):
549 return self._get_proxy(path, self, path, exclusive)
550
551 def _get_proxy(self, path, root, full_path, exclusive):
552 while path.startswith('/'):
553 path = path[1:]
554 if not path:
555 return self._mk_proxy(root, full_path, exclusive)
556
557 # need to escalate
558 rev = self._branches[None]._latest
559 name, _, path = path.partition('/')
560 field = children_fields(self._type)[name]
561 if field.is_container:
562 if not path:
563 raise ValueError('Cannot proxy a container field')
564 if field.key:
565 key, _, path = path.partition('/')
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800566 key = field.key_from_str(key)
567 children = rev._children[name]
568 _, child_rev = find_rev_by_key(children, field.key, key)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800569 child_node = child_rev.node
570 return child_node._get_proxy(path, root, full_path, exclusive)
571
572 raise ValueError('Cannot index into container with no keys')
573
574 else:
575 child_rev = rev._children[name][0]
576 child_node = child_rev.node
577 return child_node._get_proxy(path, root, full_path, exclusive)
578
579 def _mk_proxy(self, root, full_path, exclusive):
580 if self._proxy is None:
581 self._proxy = ConfigProxy(root, self, full_path, exclusive)
582 else:
583 if self._proxy.exclusive:
584 raise ValueError('Node is already owned exclusively')
585 return self._proxy
Zsolt Harasztib7067842016-11-22 18:11:53 -0800586
alshabibfa007ce2017-02-22 22:19:51 -0800587 def _mk_event_bus(self):
588 if self._event_bus is None:
589 self._event_bus = ConfigEventBus()
590 return self._event_bus
591
Zsolt Harasztib7067842016-11-22 18:11:53 -0800592 # ~~~~~~~~~~~~~~~~~~~~~~~~ Persistence loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
593
594 def load_latest(self, latest_hash):
595
596 root = self._root
597 kv_store = root._kv_store
598
599 branch = ConfigBranch(self, self._auto_prune)
600 rev = PersistedConfigRevision.load(
601 branch, kv_store, self._type, latest_hash)
602 self._make_latest(branch, rev)
603 self._branches[None] = branch