Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 1 | # |
Zsolt Haraszti | 3eb27a5 | 2017-01-03 21:56:48 -0800 | [diff] [blame] | 2 | # Copyright 2017 the original author or authors. |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 16 | from copy import copy |
| 17 | |
| 18 | from jsonpatch import JsonPatch |
| 19 | from jsonpatch import make_patch |
| 20 | |
| 21 | from common.utils.json_format import MessageToDict |
| 22 | from voltha.core.config.config_branch import ConfigBranch |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 23 | from voltha.core.config.config_event_bus import ConfigEventBus |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 24 | from voltha.core.config.config_proxy import CallbackType, ConfigProxy |
| 25 | from voltha.core.config.config_rev import is_proto_message, children_fields, \ |
| 26 | ConfigRevision, access_rights |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 27 | from voltha.core.config.config_rev_persisted import PersistedConfigRevision |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 28 | from voltha.core.config.merge_3way import merge_3way |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 29 | from voltha.protos import third_party |
| 30 | from voltha.protos import meta_pb2 |
| 31 | |
| 32 | |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 33 | def message_to_dict(m): |
| 34 | return MessageToDict(m, True, True, False) |
| 35 | |
| 36 | |
| 37 | def 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 50 | def 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 57 | class 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 Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 67 | '_root', # ref to root node |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 68 | '_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 |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 73 | '_event_bus', # ref to event_bus or None if no event bus is assigned |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 74 | '_auto_prune' |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 75 | ) |
| 76 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 77 | def __init__(self, root, initial_data, auto_prune=True, txid=None): |
| 78 | self._root = root |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 79 | self._branches = {} |
| 80 | self._tags = {} |
| 81 | self._proxy = None |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 82 | self._event_bus = None |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 83 | self._auto_prune = auto_prune |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 84 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 85 | if isinstance(initial_data, type): |
| 86 | self._type = initial_data |
| 87 | elif is_proto_message(initial_data): |
| 88 | self._type = initial_data.__class__ |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 89 | copied_data = initial_data.__class__() |
| 90 | copied_data.CopyFrom(initial_data) |
| 91 | self._initialize(copied_data, txid) |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 92 | else: |
| 93 | raise NotImplementedError() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 94 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 95 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 102 | # 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 109 | keys_seen = set() |
| 110 | children[field_name] = lst = [] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 111 | for v in field_value: |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 112 | rev = self._mknode(v, txid=txid).latest |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 113 | key = getattr(v, field.key) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 114 | if key in keys_seen: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 115 | raise ValueError('Duplicate key "{}"'.format(key)) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 116 | lst.append(rev) |
| 117 | keys_seen.add(key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 118 | else: |
| 119 | children[field_name] = [ |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 120 | self._mknode(v, txid=txid).latest for v in field_value] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 121 | else: |
| 122 | children[field_name] = [ |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 123 | self._mknode(field_value, txid=txid).latest] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 124 | data.ClearField(field_name) |
| 125 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 126 | branch = ConfigBranch(self, auto_prune=self._auto_prune) |
| 127 | rev = self._mkrev(branch, data, children) |
| 128 | self._make_latest(branch, rev) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 129 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 179 | children = rev._children[name] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 180 | if path: |
| 181 | # need to escalate further |
| 182 | key, _, path = path.partition('/') |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 183 | key = field.key_from_str(key) |
| 184 | _, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 185 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 190 | for child_rev in children: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 191 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 240 | children = copy(rev._children[name]) |
| 241 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 242 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 250 | children[idx] = new_child_rev |
| 251 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 252 | 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 |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 292 | 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 Nursimulu | 56b3647 | 2017-03-08 15:32:42 -0500 | [diff] [blame^] | 307 | for change_type, data in change_announcements: |
| 308 | self._root.enqueue_notification_callback( |
| 309 | self._mk_event_bus().advertise, |
| 310 | change_type, |
| 311 | data, |
| 312 | hash=rev.hash, |
| 313 | ) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 314 | |
| 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 338 | children = copy(rev._children[name]) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 339 | key = getattr(data, field.key) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 340 | try: |
| 341 | find_rev_by_key(children, field.key, key) |
| 342 | except KeyError: |
| 343 | pass |
| 344 | else: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 345 | raise ValueError('Duplicate key "{}"'.format(key)) |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 346 | child_rev = self._mknode(data).latest |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 347 | children.append(child_rev) |
| 348 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 349 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 360 | children = copy(rev._children[name]) |
| 361 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 362 | child_node = child_rev.node |
| 363 | new_child_rev = child_node.add(path, data, txid, mk_branch) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 364 | children[idx] = new_child_rev |
| 365 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 366 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 398 | children = copy(rev._children[name]) |
| 399 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 400 | child_node = child_rev.node |
| 401 | new_child_rev = child_node.remove(path, txid, mk_branch) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 402 | children[idx] = new_child_rev |
| 403 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 404 | self._make_latest(branch, rev) |
| 405 | return rev |
| 406 | else: |
| 407 | # need to remove from this very node |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 408 | children = copy(rev._children[name]) |
| 409 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 410 | if self._proxy is not None: |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 411 | data = child_rev.data |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 412 | self._proxy.invoke_callbacks( |
| 413 | CallbackType.PRE_REMOVE, data) |
| 414 | post_anno = ((CallbackType.POST_REMOVE, data),) |
| 415 | else: |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 416 | post_anno = ((CallbackType.POST_REMOVE, child_rev.data),) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 417 | del children[idx] |
| 418 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 419 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 437 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 445 | def merge_child(child_rev): |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 446 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 451 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 458 | rev, changes = merge_3way( |
| 459 | fork_rev, src_rev, dst_rev, merge_child, dry_run) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 460 | |
| 461 | if not dry_run: |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 462 | self._make_latest(dst_branch, rev, change_announcements=changes) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 463 | del self._branches[txid] |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 464 | |
| 465 | return rev |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 466 | |
| 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame] | 486 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 487 | 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame] | 506 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 507 | |
| 508 | def delete_tags(self, *tags): |
| 509 | for tag in tags: |
| 510 | del self._tags[tag] |
Ryan Van Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame] | 511 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 512 | |
| 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame] | 522 | def persist_tags(self): |
| 523 | """ |
| 524 | Persist tag information to the backend |
| 525 | """ |
| 526 | |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 527 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 561 | key = field.key_from_str(key) |
| 562 | children = rev._children[name] |
| 563 | _, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 564 | 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 Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 581 | |
alshabib | fa007ce | 2017-02-22 22:19:51 -0800 | [diff] [blame] | 582 | def _mk_event_bus(self): |
| 583 | if self._event_bus is None: |
| 584 | self._event_bus = ConfigEventBus() |
| 585 | return self._event_bus |
| 586 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 587 | # ~~~~~~~~~~~~~~~~~~~~~~~~ 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 |