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 |
| 23 | from voltha.core.config.config_proxy import CallbackType, ConfigProxy |
| 24 | from voltha.core.config.config_rev import is_proto_message, children_fields, \ |
| 25 | ConfigRevision, access_rights |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 26 | from voltha.core.config.config_rev_persisted import PersistedConfigRevision |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 27 | from voltha.core.config.merge_3way import merge_3way |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 28 | from voltha.protos import third_party |
| 29 | from voltha.protos import meta_pb2 |
| 30 | |
| 31 | |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 32 | def message_to_dict(m): |
| 33 | return MessageToDict(m, True, True, False) |
| 34 | |
| 35 | |
| 36 | def 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 49 | def 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 56 | class 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 Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 66 | '_root', # ref to root node |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 67 | '_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 Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 72 | '_auto_prune' |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 73 | ) |
| 74 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 75 | def __init__(self, root, initial_data, auto_prune=True, txid=None): |
| 76 | self._root = root |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 77 | self._branches = {} |
| 78 | self._tags = {} |
| 79 | self._proxy = None |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 80 | self._auto_prune = auto_prune |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 81 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 82 | if isinstance(initial_data, type): |
| 83 | self._type = initial_data |
| 84 | elif is_proto_message(initial_data): |
| 85 | self._type = initial_data.__class__ |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 86 | copied_data = initial_data.__class__() |
| 87 | copied_data.CopyFrom(initial_data) |
| 88 | self._initialize(copied_data, txid) |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 89 | else: |
| 90 | raise NotImplementedError() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 91 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 92 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 99 | # 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 106 | keys_seen = set() |
| 107 | children[field_name] = lst = [] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 108 | for v in field_value: |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 109 | rev = self._mknode(v, txid=txid).latest |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 110 | key = getattr(v, field.key) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 111 | if key in keys_seen: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 112 | raise ValueError('Duplicate key "{}"'.format(key)) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 113 | lst.append(rev) |
| 114 | keys_seen.add(key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 115 | else: |
| 116 | children[field_name] = [ |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 117 | self._mknode(v, txid=txid).latest for v in field_value] |
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(field_value, txid=txid).latest] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 121 | data.ClearField(field_name) |
| 122 | |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 123 | branch = ConfigBranch(self, auto_prune=self._auto_prune) |
| 124 | rev = self._mkrev(branch, data, children) |
| 125 | self._make_latest(branch, rev) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 126 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 176 | children = rev._children[name] |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 177 | if path: |
| 178 | # need to escalate further |
| 179 | key, _, path = path.partition('/') |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 180 | key = field.key_from_str(key) |
| 181 | _, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 182 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 187 | for child_rev in children: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 188 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 237 | children = copy(rev._children[name]) |
| 238 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 239 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 247 | children[idx] = new_child_rev |
| 248 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 249 | 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 Haraszti | 6686203 | 2016-11-28 14:28:39 -0800 | [diff] [blame] | 292 | # 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 302 | |
| 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 326 | children = copy(rev._children[name]) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 327 | key = getattr(data, field.key) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 328 | try: |
| 329 | find_rev_by_key(children, field.key, key) |
| 330 | except KeyError: |
| 331 | pass |
| 332 | else: |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 333 | raise ValueError('Duplicate key "{}"'.format(key)) |
Zsolt Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 334 | child_rev = self._mknode(data).latest |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 335 | children.append(child_rev) |
| 336 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 337 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 348 | children = copy(rev._children[name]) |
| 349 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 350 | child_node = child_rev.node |
| 351 | new_child_rev = child_node.add(path, data, txid, mk_branch) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 352 | children[idx] = new_child_rev |
| 353 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 354 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 386 | children = copy(rev._children[name]) |
| 387 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 388 | child_node = child_rev.node |
| 389 | new_child_rev = child_node.remove(path, txid, mk_branch) |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 390 | children[idx] = new_child_rev |
| 391 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 392 | self._make_latest(branch, rev) |
| 393 | return rev |
| 394 | else: |
| 395 | # need to remove from this very node |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 396 | children = copy(rev._children[name]) |
| 397 | idx, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 398 | if self._proxy is not None: |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 399 | data = child_rev.data |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 400 | self._proxy.invoke_callbacks( |
| 401 | CallbackType.PRE_REMOVE, data) |
| 402 | post_anno = ((CallbackType.POST_REMOVE, data),) |
| 403 | else: |
| 404 | post_anno = () |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 405 | del children[idx] |
| 406 | rev = rev.update_children(name, children, branch) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 407 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 425 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 433 | def merge_child(child_rev): |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 434 | 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 Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 439 | 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 446 | rev, changes = merge_3way( |
| 447 | fork_rev, src_rev, dst_rev, merge_child, dry_run) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 448 | |
| 449 | if not dry_run: |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 450 | self._make_latest(dst_branch, rev, change_announcements=changes) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 451 | del self._branches[txid] |
Zsolt Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 452 | |
| 453 | return rev |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 454 | |
| 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame^] | 474 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 475 | 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame^] | 494 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 495 | |
| 496 | def delete_tags(self, *tags): |
| 497 | for tag in tags: |
| 498 | del self._tags[tag] |
Ryan Van Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame^] | 499 | self.persist_tags() |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 500 | |
| 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 Gilder | bf974d0 | 2017-02-24 15:01:22 -0800 | [diff] [blame^] | 510 | def persist_tags(self): |
| 511 | """ |
| 512 | Persist tag information to the backend |
| 513 | """ |
| 514 | |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 515 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 Haraszti | 00d9a84 | 2016-11-23 11:18:23 -0800 | [diff] [blame] | 549 | key = field.key_from_str(key) |
| 550 | children = rev._children[name] |
| 551 | _, child_rev = find_rev_by_key(children, field.key, key) |
Zsolt Haraszti | dafefe1 | 2016-11-14 21:29:58 -0800 | [diff] [blame] | 552 | 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 Haraszti | b706784 | 2016-11-22 18:11:53 -0800 | [diff] [blame] | 569 | |
| 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 |