blob: 556cdd950b41a498098661b9d7bbd5601e937f41 [file] [log] [blame]
Zack Williams41513bf2018-07-07 20:08:35 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
Zsolt Harasztidafefe12016-11-14 21:29:58 -080014from collections import OrderedDict
15from copy import copy
16import resource
17from random import randint, seed
18from time import time
Zsolt Harasztib7067842016-11-22 18:11:53 -080019from unittest import main, TestCase
Zsolt Harasztidafefe12016-11-14 21:29:58 -080020
21import gc
22
alshabibfa007ce2017-02-22 22:19:51 -080023from google.protobuf.json_format import MessageToDict
Zsolt Harasztidafefe12016-11-14 21:29:58 -080024from mock import Mock
alshabibfa007ce2017-02-22 22:19:51 -080025from simplejson import dumps
Zsolt Harasztidafefe12016-11-14 21:29:58 -080026
alshabibfa007ce2017-02-22 22:19:51 -080027from common.event_bus import EventBusClient
28from voltha.core.config.config_proxy import CallbackType
Zsolt Harasztidafefe12016-11-14 21:29:58 -080029from voltha.core.config.config_rev import _rev_cache
30from voltha.core.config.config_root import ConfigRoot, MergeConflictException
31from voltha.core.config.config_txn import ClosedTransactionError
32from voltha.protos import third_party
alshabibfa007ce2017-02-22 22:19:51 -080033from voltha.protos.events_pb2 import ConfigEvent, ConfigEventType
Zsolt Harasztidafefe12016-11-14 21:29:58 -080034from voltha.protos.openflow_13_pb2 import ofp_port
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080035from voltha.protos.voltha_pb2 import VolthaInstance, Adapter, HealthStatus, \
Zsolt Haraszti66862032016-11-28 14:28:39 -080036 AdapterConfig, LogicalDevice, LogicalPort
Zsolt Harasztidafefe12016-11-14 21:29:58 -080037
38
39def memusage():
40 return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
41
42
43def rev_count():
44 return len(_rev_cache)
45
46
47def probe():
48 return time(), memusage() / 1024. / 1024, rev_count()
49
50
51def print_metrics():
52 print '%20f %20f %8d' % probe()
53
54
55class TestConfigNodeShallow(TestCase):
56
57 def setUp(self):
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080058 self.empty = VolthaInstance()
59 self.other = VolthaInstance(instance_id='other')
60 self.node = ConfigRoot(VolthaInstance())
Zsolt Harasztidafefe12016-11-14 21:29:58 -080061
62 def test_init(self):
63 pass
64
65 def test_immutability(self):
66 self.assertEqual(self.node.latest.data, self.empty)
67 self.empty.instance_id = 'overwritten id'
Zsolt Haraszti00d9a842016-11-23 11:18:23 -080068 self.assertEqual(self.node.latest.data, VolthaInstance())
Zsolt Harasztidafefe12016-11-14 21:29:58 -080069
70 def test_retrieve_latest(self):
71 self.assertEqual(self.node.latest.data, self.empty)
72 hash = self.node.latest.hash
73 self.assertEqual(self.node.revisions, [hash])
74
Matt Jeanneret58f7a552018-10-02 07:00:27 -040075 def test_update(self):
76 hash0 = self.node.latest.hash
77 self.node.update('/', copy(self.other))
78 hash1 = self.node.latest.hash
79 self.assertEqual(len(self.node.revisions), 2)
80 self.assertNotEqual(hash0, hash1)
81 self.assertEqual(self.node.latest.data, VolthaInstance(instance_id='other'))
82
Zsolt Harasztidafefe12016-11-14 21:29:58 -080083 def test_update_with_bad_data(self):
84 self.assertRaises(ValueError, self.node.update, '/', Adapter())
85
Matt Jeanneret58f7a552018-10-02 07:00:27 -040086 def test_many_simple_updates(self):
87 n = 1000
88 for i in xrange(n):
89 self.node.update('/', VolthaInstance(instance_id='id%d' % i))
90 self.node.update('/', self.other)
91 self.assertEqual(len(self.node.revisions), 1002)
92 self.assertEqual(self.node.latest.data, self.other)
93
94 def test_retrieve_by_rev_hash(self):
95 n = 1000
96 for i in xrange(n):
97 self.node.update('/', VolthaInstance(instance_id='id%d' % i))
98 self.node.update('/', self.other)
99 hashes = self.node.revisions
100 self.assertEqual(self.node[hashes[0]].data, self.empty)
101 self.assertEqual(self.node[hashes[10]].data, VolthaInstance(instance_id='id9'))
102 self.assertEqual(self.node[hashes[-1]].data, self.other)
103
104 def test_diffs(self):
105 self.node.update('/', self.other)
106 self.assertEqual(self.node.diff(self.node.latest.hash).patch, [])
107 hashes = self.node.revisions
108 self.assertEqual(self.node.diff(hashes[0]).patch, [
109 dict(op='replace', path='/instance_id', value='other')
110 ])
111 self.assertEqual(self.node.diff(hashes[0], hashes[1]).patch, [
112 dict(op='replace', path='/instance_id', value='other')
113 ])
114 self.assertEqual(self.node.diff(hashes[1], hashes[0]).patch, [
115 dict(op='replace', path='/instance_id', value='')
116 ])
117 self.assertEqual(self.node.diff(hashes[1], hashes[1]).patch, [])
118
119 def test_tagging(self):
120 self.node.tag('original')
121 hash1 = self.node.latest.hash
122
123 # add a bunch of changes
124 for a in xrange(10):
125 self.node.update('/', VolthaInstance(instance_id=str(a)))
126 hash2 = self.node.latest.hash
127
128 # apply tag to latest
129 self.node.tag('latest')
130
131 # apply another tag to latest
132 self.node.tag('other')
133
134 # apply tag to specific rev hash
135 self.node.tag('yetmore', hash2)
136
137 # invalid hash
138 self.assertRaises(KeyError, self.node.tag, 'sometag', 'badhash')
139
140 # retrieve data based on tag
141 self.assertEqual(self.node.by_tag('original').hash, hash1)
142 self.assertEqual(self.node.by_tag('latest').hash, hash2)
143 self.assertEqual(self.node.by_tag('other').hash, hash2)
144 self.assertEqual(self.node.by_tag('yetmore').hash, hash2)
145
146 # generate diff from tags
147 self.assertEqual(self.node.diff_by_tag('original', 'latest').patch, [
148 dict(op='replace', path='/instance_id', value='9')
149 ])
150
151 # move tags to given revision
152 self.node.tag('original', self.node.revisions[2])
153 self.node.tag('latest', self.node.revisions[9])
154
155 # add another tag
156 self.node.tag('another', self.node.revisions[7])
157
158 # list tags
159 self.assertEqual(self.node.tags,
160 ['another', 'latest', 'original', 'other', 'yetmore'])
161
162 # delete a tag
163 self.node.delete_tag('another')
164 self.node.delete_tags('yetmore', 'other')
165 self.assertEqual(self.node.tags, ['latest', 'original'])
166
167 # prune untagged revisions from revision list
168 self.node.prune_untagged()
169 self.assertEqual(len(self.node.revisions), 3) # latest is always kept
170
171 # retrieve and compare working tagged revs
172 self.assertEqual(self.node.diff_by_tag('original', 'latest').patch, [
173 dict(op='replace', path='/instance_id', value='8')
174 ])
175
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800176
177class DeepTestsBase(TestCase):
178 """Shared test class for test using a simple node tree"""
179
180 def setUp(self):
181 gc.collect()
182 _rev_cache.clear()
183 self.health = HealthStatus(state=HealthStatus.DYING)
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800184 self.base_shallow = VolthaInstance(instance_id='1')
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800185 self.base_deep = copy(self.base_shallow)
186 self.base_deep.health.state = HealthStatus.DYING # = self.health
187 for i in xrange(5):
188 self.base_deep.adapters.add().MergeFrom(Adapter(
189 id=str(i),
190 config=AdapterConfig(
191 log_level=3
192 )
193 ))
194 self.node = ConfigRoot(self.base_deep)
195 self.hash_orig = self.node.latest.hash
196
197 def tearDown(self):
198 del self.node
199
200
201class TestConfigNodeDeep(DeepTestsBase):
202
203 def test_init(self):
204 pass
205
206 def test_reject_duplicate_keys(self):
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800207 data = VolthaInstance(
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800208 instance_id='42', adapters=[Adapter(id='same') for _ in xrange(5)])
209 self.assertRaises(ValueError, ConfigRoot, data)
210
211 def test_shallow_get(self):
212 self.assertEqual(self.node.latest.data, self.base_shallow)
213 self.assertEqual(self.node.get(), self.base_shallow)
214 self.assertEqual(self.node.get(hash=self.hash_orig), self.base_shallow)
215
216 def test_deep_get(self):
217 self.assertEqual(self.node.get(deep=True), self.base_deep)
218
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400219 def test_top_level_update(self):
220 # test that top-level update retains children
221 self.node.update('/', VolthaInstance(version='1.2.3'))
222 hash_new = self.node.latest.hash
223 self.assertNotEqual(self.hash_orig, hash_new)
224 self.assertEqual(self.node.get(
225 hash=self.hash_orig, deep=1), self.base_deep)
226 latest = self.node.get(deep=1)
227 self.assertNotEqual(latest, self.base_deep)
228 self.assertEqual(len(latest.adapters), 5)
229 self.assertEqual(len(latest.logical_devices), 0)
230 self.assertEqual(latest.version, '1.2.3')
231
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800232 def test_path_based_get_access(self):
233 self.assertEqual(self.node.get(path='/'), self.node.get())
234 self.assertEqual(self.node.get(path='/health'), self.health)
235
236 def test_path_list_retrieval(self):
237 adapters = self.node.get(path='/adapters')
238 self.assertEqual(len(adapters), 5)
239 self.assertEqual(adapters[2].id, '2')
240
241 def test_indexing_into_containers(self):
242 adapter = self.node.get(path='/adapters/3')
243 self.assertEqual(adapter.id, '3')
244
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400245 def test_deep_update_non_container(self):
246
247 self.node.update('/health', HealthStatus(state=HealthStatus.HEALTHY))
248
249 # root hash is now different
250 hash_new = self.node.latest.hash
251 self.assertNotEqual(self.hash_orig, hash_new)
252
253 # original tree is still intact
254 orig = self.node.get(hash=self.hash_orig, deep=1)
255 self.assertEqual(orig, self.base_deep)
256 self.assertEqual(orig.health.state, HealthStatus.DYING)
257
258 # but the latest contains the change
259 new = self.node.get(deep=1)
260 self.assertNotEqual(new, orig)
261 self.assertEqual(new.health.state, HealthStatus.HEALTHY)
262
263 def test_deep_update_container(self):
264
265 self.node.update('/adapters/0', Adapter(id='0', version='new'))
266
267 # root hash is now different
268 hash_new = self.node.latest.hash
269 self.assertNotEqual(self.hash_orig, hash_new)
270
271 # original tree is still intact
272 orig = self.node.get(hash=self.hash_orig, deep=1)
273 self.assertEqual(orig, self.base_deep)
274 self.assertEqual(orig.adapters[0].id, '0')
275
276 # but the new tree contains the change
277 new = self.node.get(deep=1)
278 self.assertNotEqual(new, orig)
279 self.assertEqual(new.adapters[0].version, 'new')
280
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800281 def test_update_handle_invalid_paths(self):
282 self.assertRaises(KeyError, self.node.update, 'foo', None)
283 self.assertRaises(KeyError, self.node.update, '/foo', None)
284 self.assertRaises(KeyError, self.node.update, '/health/foo', None)
285 self.assertRaises(ValueError, self.node.update, '/adapters', None)
286 self.assertRaises(KeyError, self.node.update, '/adapters/foo', None)
287 self.assertRaises(KeyError, self.node.update, '/adapters/1/foo', None)
288
289 def test_update_handle_invalid_type(self):
290 self.assertRaises(ValueError, self.node.update, '/', Adapter())
291 self.assertRaises(ValueError, self.node.update, '/health', Adapter())
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800292 self.assertRaises(ValueError, self.node.update, '/adapters/1', VolthaInstance())
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800293
294 def test_update_handle_key_change_attempt(self):
295 self.assertRaises(
296 ValueError, self.node.update, '/adapters/1', Adapter(id='changed'))
297
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400298 def test_add_node(self):
299 new = Adapter(id='new')
300 self.node.add('/adapters', new)
301 self.assertNotEqual(self.node.latest.hash, self.hash_orig)
302 self.assertEqual(len(self.node.get('/adapters')), 6)
303 self.assertEqual(
304 len(self.node.get('/adapters', hash=self.hash_orig)), 5)
305 self.assertEqual(self.node.get('/adapters/new'), new)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800306
307 def test_add_handle_invalid_cases(self):
308 # invalid paths
309 self.assertRaises(KeyError, self.node.add, 'foo', None)
310 self.assertRaises(KeyError, self.node.add, '/foo', None)
311 self.assertRaises(KeyError, self.node.add, '/adapters/foo', None)
312
313 # cannot add to non-container nodes
314 self.assertRaises(ValueError, self.node.add, '/health', None)
315 self.assertRaises(ValueError, self.node.add, '/adapters/1', None)
316
317 # cannot add to container data with duplicate key
318 self.assertRaises(
319 ValueError, self.node.add, '/adapters', Adapter(id='1'))
320
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400321 def test_remove_node(self):
322 self.node.remove('/adapters/3')
323 self.assertNotEqual(self.node.latest.hash, self.hash_orig)
324 self.assertEqual(len(self.node.get('/adapters')), 4)
325 self.assertEqual(
326 len(self.node.get('/adapters', hash=self.hash_orig)), 5)
327 self.assertRaises(KeyError, self.node.get, '/adapters/3')
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800328
329 def test_remove_handle_invalid_cases(self):
330 # invalid paths
331 self.assertRaises(KeyError, self.node.remove, 'foo')
332 self.assertRaises(KeyError, self.node.remove, '/foo')
333 self.assertRaises(KeyError, self.node.remove, '/adapters/foo')
334 self.assertRaises(KeyError, self.node.remove, '/adapters/1/id')
335
336 # cannot add to non-container nodes
337 self.assertRaises(ValueError, self.node.remove, '/health')
338
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400339 def test_pruning_after_shallow_change(self):
340
341 self.node.update('/', VolthaInstance(version='10.1'))
342
343 # sanity check
344 self.assertEqual(len(self.node.revisions), 2)
345
346 # prune
347 self.node.prune_untagged()
348
349 self.assertEqual(len(self.node.revisions), 1)
350
351 # we can nevertheless access the whole tree
352 new = self.node.get('/', deep=1)
353 self.assertEqual(new.adapters, self.base_deep.adapters)
354 self.assertEqual(new.version, '10.1')
355
356 def test_pruning_after_deep_change(self):
357
358 self.node.update('/adapters/3', Adapter(id='3', version='changed'))
359
360 # sanity check
361 self.assertEqual(len(self.node.revisions), 2)
362
363 # prune
364 self.node.prune_untagged()
365
366 self.assertEqual(len(self.node.revisions), 1)
367
368 # we can nevertheless access the whole tree
369 new = self.node.get('/', deep=1)
370 self.assertEqual(len(new.adapters), 5)
371 self.assertEqual(new.adapters[2], self.base_deep.adapters[2])
372 self.assertEqual(new.adapters[3].version, 'changed')
373
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800374
375class TestPruningPerformance(DeepTestsBase):
376
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400377 def test_repeated_prunning_keeps_memory_stable(self):
378 # The auto-pruning feature of the config system is that leaf nodes
379 # in the config tree that are no longer in use are pruned from memory,
380 # once they revision is removed from root using the .prune_untagged()
381 # method. This test is to verify that.
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800382
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400383 n = 1000
384
385 seed(0) # makes things consistently random
386
387 # this should be the number of nodes in the VolthaInstance tree
388 self.assertLess(rev_count(), 20)
389 print; print_metrics()
390
391 def mk_change():
392 key = str(randint(0, 4))
393 path = '/adapters/' + key
394 adapter = self.node.get(path)
395 adapter.version = 'v{}'.format(randint(0, 100000))
396 self.node.update(path, adapter)
397
398 # first we perform many changes without pruning
399 for i in xrange(n):
400 mk_change()
401
402 # at this point we shall have more than 2*n revs laying around
403 self.assertGreater(rev_count(), 2 * n)
404 print_metrics()
405
406 # prune now
407 self.node.prune_untagged()
408
409 # at this point the rev count shall fall back to the original
410 self.assertLess(rev_count(), 15)
411 print_metrics()
412
413 # no make an additional set of modifications while constantly pruning
414 for i in xrange(n):
415 mk_change()
416 self.node.prune_untagged()
417
418 # the rev count should not have increased
419 self.assertLess(rev_count(), 15)
420 print_metrics()
421
422 def test_churn_efficiency(self):
423 # Two config revisions that hash to the same hash value also share the
424 # same in-memory object. So if the same config node goes through churn
425 # (like flip-flopping fields), we don't eat up memory unnecessarily.
426 # This test is to verify that behavior.
427
428 n = 1000
429 modulo = 2
430
431 self.assertEqual(rev_count(), 14)
432 print_metrics()
433
434 def mk_change(seq):
435 # make change module of the sequence number so we periodically
436 # return to the same config
437 path = '/adapters/3'
438 adapter = self.node.get(path)
439 adapter.version = 'v{}'.format(seq % modulo)
440 self.node.update(path, adapter)
441
442 # make n changes back and forth
443 for i in xrange(n):
444 _tmp_rc = rev_count()
445 mk_change(i)
446
447 _tmp_rc = rev_count()
448 # verify that the node count did not increase significantly, yet we
449 # have access to all ditinct revisions
450 self.assertEqual(rev_count(), 20)
451 print_metrics()
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800452
453 def test_strict_read_only(self):
454 # it shall not be possible to change a read-only field
455 self.assertRaises(ValueError, self.node.update,
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800456 '/', VolthaInstance(version='foo'), strict=True)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800457 self.assertRaises(ValueError, self.node.update,
458 '/adapters/1', Adapter(version='foo'), strict=True)
459
460
461class TestNodeOwnershipAndHooks(DeepTestsBase):
462
463 def test_init(self):
464 pass
465
466 def test_passive_ownership(self):
467
468 # grab a proxy for a given node
469 proxy = self.node.get_proxy('/health')
470
471 # able to read the value directly using this proxy
472 self.assertEqual(proxy.get(), HealthStatus(state=HealthStatus.DYING))
473
474 # able to update value directly using this proxy, but the whole tree
475 # updates
476 proxy.update('/', HealthStatus(state=HealthStatus.HEALTHY))
477 self.assertEqual(proxy.get().state, HealthStatus.HEALTHY)
478 self.assertNotEqual(self.node.latest.hash, self.hash_orig)
479
480 # access constraints are still enforced
481 self.assertRaises(
482 ValueError, proxy.update,
483 '/', HealthStatus(state=HealthStatus.OVERLOADED), strict=1)
484
485 def test_exclusivity(self):
486 proxy = self.node.get_proxy('/adapters/1', exclusive=True)
487 self.assertRaises(ValueError, self.node.get_proxy, '/adapters/1')
488
489 def test_get_hook(self):
490
491 proxy = self.node.get_proxy('/health')
492
493 # getting health without callback just returns what's stored in node
494 self.assertEqual(proxy.get().state, HealthStatus.DYING)
495
496 # register callback
497 def get_health_callback(msg):
498 msg.state = HealthStatus.OVERLOADED
499 return msg
500
501 proxy.register_callback(CallbackType.GET, get_health_callback)
502
503 # once registered, callback can touch up object
504 self.assertEqual(proxy.get().state, HealthStatus.OVERLOADED)
505
506 def test_pre_update_hook(self):
507
508 proxy = self.node.get_proxy('/adapters/1')
509
510 # before hook, change is allowed
511 adapter = proxy.get()
512 adapter.version = 'foo'
513 proxy.update('/', adapter)
514
515 # and for sanity, check if update made it through
516 self.assertEqual(self.node.get('/adapters/1').version, 'foo')
517
518 # regsiter hook that rejects all changes
519 def bully(msg):
520 raise RuntimeError('bully')
521 proxy.register_callback(CallbackType.PRE_UPDATE, bully)
522
523 # test that rejection applies
524 adapter.version = 'bar'
525 self.assertRaises(RuntimeError, proxy.update, '/', adapter)
526 self.assertRaises(RuntimeError, self.node.update, '/adapters/1', adapter)
527
528 def test_post_update_hook(self):
529 proxy = self.node.get_proxy('/adapters/1')
530 callback = Mock()
531 proxy.register_callback(CallbackType.POST_UPDATE, callback,
532 'zizi', 42, x=1, y='baz')
533 data = Adapter(id='1', version='zoo')
534 proxy.update('/', data)
535 callback.assert_called_once_with(data, 'zizi', 42, x=1, y='baz')
536
537 def test_pre_and_post_add_hooks(self):
538 proxy = self.node.get_proxy('/')
539 pre_callback = Mock()
540 post_callback = Mock()
541 proxy.register_callback(CallbackType.PRE_ADD, pre_callback)
542 proxy.register_callback(CallbackType.POST_ADD, post_callback)
543 new_adapter = Adapter(id='99', version='12.2', vendor='ace')
544 proxy.add('/adapters', new_adapter)
545 pre_callback.assert_called_with(new_adapter)
546 post_callback.assert_called_with(new_adapter)
547
548 def test_pre_and_post_remove_hooks(self):
549 proxy = self.node.get_proxy('/')
550 pre_callback = Mock()
551 post_callback = Mock()
552 proxy.register_callback(CallbackType.PRE_REMOVE, pre_callback)
553 proxy.register_callback(CallbackType.POST_REMOVE, post_callback)
554 adapter = proxy.get('/adapters/1') # so that we can verify callback
555 proxy.remove('/adapters/1')
556 pre_callback.assert_called_with(adapter)
557 post_callback.assert_called_with(adapter)
558
alshabibfa007ce2017-02-22 22:19:51 -0800559class TestEventLogic(DeepTestsBase):
560
561 def setUp(self):
562 super(TestEventLogic, self).setUp()
563 self.ebc = EventBusClient()
564 self.event_mock = Mock()
565 self.ebc.subscribe('model-change-events', self.event_mock)
566
567 def test_add_event(self):
568
569 data = Adapter(id='10', version='zoo')
570 self.node.add('/adapters', data)
571 event = ConfigEvent(
572 type=ConfigEventType.add,
573 hash=self.node.latest.hash,
574 data=dumps(MessageToDict(data, True, True))
575 )
576
Khen Nursimulu56b36472017-03-08 15:32:42 -0500577 self.event_mock.assert_called_once_with('model-change-events', event)
alshabibfa007ce2017-02-22 22:19:51 -0800578
579 def test_remove_event(self):
580 data = Adapter(
581 id='1',
582 config=AdapterConfig(
583 log_level=3
584 )
585 )
586 self.node.remove('/adapters/1')
587 event = ConfigEvent(
588 type=ConfigEventType.remove,
589 hash=self.node.latest.hash,
590 data=dumps(MessageToDict(data, True, True))
591 )
592
Khen Nursimulu56b36472017-03-08 15:32:42 -0500593 self.event_mock.assert_called_once_with('model-change-events', event)
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800594
595class TestTransactionalLogic(DeepTestsBase):
596
597 def make_change(self, tx, path, attr_name, new_value):
598 data = o = tx.get(path)
599 rest = attr_name
600 while 1:
601 subfield, _, rest = rest.partition('.')
602 if rest:
603 o = getattr(o, subfield)
604 attr_name = rest
605 else:
606 setattr(o, attr_name, new_value)
607 break
608 tx.update(path, data)
609
610 def check_no_tx_branches(self):
611 visited = set()
612
613 def check_node(n):
614 if n not in visited:
615 self.assertEqual(n._branches.keys(), [None])
616 for rev in n._branches[None]._revs.itervalues():
617 for children in rev._children.itervalues():
618 if isinstance(children, OrderedDict):
619 children = children.itervalues()
620 for child_rev in children:
621 child_node = child_rev.node
622 check_node(child_node)
623 visited.add(n)
624
625 check_node(self.node)
626
627 def log_levels(self):
628 return OrderedDict(
629 (a.id, a.config.log_level)
630 for a in self.node.get('/adapters', deep=1))
631
632 def tearDown(self):
633 self.check_no_tx_branches()
634 super(TestTransactionalLogic, self).tearDown()
635
636 def test_transaction_isolation(self):
637 """
638 Test that changes made in a transaction mode are not visible to others
639 """
640 proxy = self.node.get_proxy('/')
641
642 # look under the hood to verify that branches are added
643 # recursively
644 _latest_root_rev = self.node._branches[None].latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800645 adapter_node = _latest_root_rev._children['adapters'][2].node
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800646 self.assertEqual(len(self.node._branches.keys()), 1)
647 self.assertEqual(len(adapter_node._branches.keys()), 1)
648
649 tx = proxy.open_transaction()
650 self.assertEqual(len(self.node._branches.keys()), 2)
651 self.assertEqual(len(adapter_node._branches.keys()), 1)
652
653 path = '/adapters/2'
654 self.make_change(tx, path, 'config.log_level', 0)
655 self.assertEqual(len(self.node._branches.keys()), 2)
656 self.assertEqual(len(adapter_node._branches.keys()), 2)
657
658 # verify that reading from the transaction exposes the change
659 self.assertEqual(tx.get(path).config.log_level, 0)
660
661 # but that reading from the proxy or directly from tree does not
662 self.assertEqual(self.node.latest.hash, self.hash_orig)
663 self.assertEqual(proxy.get(path).config.log_level, 3)
664 self.assertEqual(self.node.get(path).config.log_level, 3)
665
666 tx.cancel()
667
668 def test_cannot_reuse_tx(self):
669 proxy = self.node.get_proxy('/')
670 tx = proxy.open_transaction()
671 tx.cancel()
672 self.assertRaises(ClosedTransactionError, tx.get, '/')
673 self.assertRaises(ClosedTransactionError, tx.add, '/', None)
674 self.assertRaises(ClosedTransactionError, tx.remove, '/')
675
676 def test_multiple_concurrent_transactions(self):
677 """
678 Test that two transactions can make independent changes, without
679 affecting each other, until committed.
680 """
681 proxy1 = self.node.get_proxy('/')
682 proxy2 = self.node.get_proxy('/')
683
684 tx1 = proxy1.open_transaction()
685 tx2 = proxy1.open_transaction()
686 tx3 = proxy2.open_transaction()
687
688 path = '/adapters/2'
689 self.make_change(tx1, path, 'config.log_level', 0)
690
691 # the other transaction does not see the change
692 self.assertEqual(tx2.get(path).config.log_level, 3)
693 self.assertEqual(tx3.get(path).config.log_level, 3)
694 self.assertEqual(proxy1.get(path).config.log_level, 3)
695 self.assertEqual(proxy2.get(path).config.log_level, 3)
696
697 # we can attempt to make change in other txs
698 self.make_change(tx2, path, 'config.log_level', 1)
699 self.make_change(tx3, path, 'config.log_level', 2)
700
701 # each can see its own tree, but no one else can see theirs
702 self.assertEqual(tx1.get(path).config.log_level, 0)
703 self.assertEqual(tx2.get(path).config.log_level, 1)
704 self.assertEqual(tx3.get(path).config.log_level, 2)
705 self.assertEqual(proxy1.get(path).config.log_level, 3)
706 self.assertEqual(proxy2.get(path).config.log_level, 3)
707 self.assertEqual(self.node.latest.hash, self.hash_orig)
708
709 tx1.cancel()
710 tx2.cancel()
711 tx3.cancel()
712
713 def test_transaction_canceling(self):
714 """After abort, transaction is no longer stored"""
715
716 proxy = self.node.get_proxy('/')
717
718 # look under the hood to verify that branches are added
719 # recursively
720 _latest_root_rev = self.node._branches[None].latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800721 adapter_node = _latest_root_rev._children['adapters'][2].node
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800722 self.assertEqual(len(self.node._branches.keys()), 1)
723 self.assertEqual(len(adapter_node._branches.keys()), 1)
724
725 tx = proxy.open_transaction()
726 self.assertEqual(len(self.node._branches.keys()), 2)
727 self.assertEqual(len(adapter_node._branches.keys()), 1)
728
729 self.make_change(tx, '/adapters/2', 'config.log_level', 4)
730
731 self.assertEqual(len(self.node._branches.keys()), 2)
732 self.assertEqual(len(adapter_node._branches.keys()), 2)
733
734 del tx
735
736 self.assertEqual(len(self.node._branches.keys()), 1)
737 self.assertEqual(len(adapter_node._branches.keys()), 1)
738
739 def test_transaction_explitic_canceling(self):
740 """After abort, transaction is no longer stored"""
741
742 proxy = self.node.get_proxy('/')
743
744 # look under the hood to verify that branches are added
745 # recursively
746 _latest_root_rev = self.node._branches[None].latest
Zsolt Haraszti00d9a842016-11-23 11:18:23 -0800747 adapter_node = _latest_root_rev._children['adapters'][2].node
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800748 self.assertEqual(len(self.node._branches.keys()), 1)
749 self.assertEqual(len(adapter_node._branches.keys()), 1)
750
751 tx = proxy.open_transaction()
752 self.assertEqual(len(self.node._branches.keys()), 2)
753 self.assertEqual(len(adapter_node._branches.keys()), 1)
754
755 self.make_change(tx, '/adapters/2', 'config.log_level', 4)
756
757 self.assertEqual(len(self.node._branches.keys()), 2)
758 self.assertEqual(len(adapter_node._branches.keys()), 2)
759
760 tx.cancel()
761
762 self.assertEqual(len(self.node._branches.keys()), 1)
763 self.assertEqual(len(adapter_node._branches.keys()), 1)
764
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400765 def test_transaction_commit(self):
766 """Once committed, changes become latest"""
Zsolt Harasztidafefe12016-11-14 21:29:58 -0800767
Matt Jeanneret58f7a552018-10-02 07:00:27 -0400768 proxy = self.node.get_proxy('/')
769 _latest_root_rev = self.node._branches[None].latest
770 adapter_node = _latest_root_rev._children['adapters'][2].node
771 tx = proxy.open_transaction()
772
773 # publicly visible value before change
774 path = '/adapters/2'
775 self.assertEqual(proxy.get(path).config.log_level, 3)
776 self.assertEqual(self.node.get(path).config.log_level, 3)
777
778 # make the change, but not commit yet
779 self.make_change(tx, path, 'config.log_level', 4)
780 self.assertEqual(proxy.get(path).config.log_level, 3)
781 self.assertEqual(self.node.get(path).config.log_level, 3)
782
783 # commit the change
784 tx.commit()
785 self.assertNotEqual(self.node.latest.hash, self.hash_orig)
786 self.assertEqual(proxy.get('/adapters/2').config.log_level, 4)
787 self.assertEqual(len(self.node._branches.keys()), 1)
788 self.assertEqual(len(adapter_node._branches.keys()), 1)
789 self.assertEqual(proxy.get(path).config.log_level, 4)
790 self.assertEqual(self.node.get(path).config.log_level, 4)
791
792 def test_collision_detection(self):
793 """Correctly detect transaction collision and abort the 2nd tx"""
794
795 proxy = self.node.get_proxy('/')
796 tx1 = proxy.open_transaction()
797 path = '/adapters/2'
798 self.make_change(tx1, path, 'config.log_level', 0)
799
800 # make another tx before tx1 is committed
801 tx2 = proxy.open_transaction()
802 print tx2._txid
803 self.make_change(tx2, path, 'config.log_level', 4)
804
805 # commit first
806 print tx1._txid
807 tx1.commit()
808
809 # committing 2nd one should fail
810 self.assertRaises(MergeConflictException, tx2.commit)
811 self.check_no_tx_branches()
812
813 def test_nonconfliciting_changes(self):
814 proxy = self.node.get_proxy('/')
815 tx1 = proxy.open_transaction()
816 tx2 = proxy.open_transaction()
817 self.make_change(tx1, '/adapters/1', 'config.log_level', 1)
818 self.make_change(tx2, '/adapters/2', 'config.log_level', 2)
819 tx1.commit()
820 tx2.commit()
821 self.assertEqual(self.log_levels(), {
822 '0': 3, '1': 1, '2': 2, '3': 3, '4': 3
823 })
824
825 def test_additive_changes(self):
826 proxy = self.node.get_proxy('/')
827 tx1 = proxy.open_transaction()
828 tx1.add('/adapters', Adapter(id='new'))
829 tx1.add('/adapters', Adapter(id='new2'))
830 self.assertEqual(len(proxy.get('/adapters')), 5)
831 self.assertEqual(len(self.node.get('/adapters')), 5)
832 self.assertEqual(len(tx1.get('/adapters')), 7)
833 tx1.commit()
834 self.assertEqual(len(proxy.get('/adapters')), 7)
835 self.assertEqual(len(self.node.get('/adapters')), 7)
836 self.assertEqual(self.log_levels().keys(),
837 ['0', '1', '2', '3', '4', 'new', 'new2'])
838
839 def test_remove_changes(self):
840 proxy = self.node.get_proxy('/')
841 tx1 = proxy.open_transaction()
842 tx1.remove('/adapters/2')
843 tx1.remove('/adapters/4')
844 self.assertEqual(len(proxy.get('/adapters')), 5)
845 self.assertEqual(len(self.node.get('/adapters')), 5)
846 self.assertEqual(len(tx1.get('/adapters')), 3)
847 tx1.commit()
848 self.assertEqual(len(proxy.get('/adapters')), 3)
849 self.assertEqual(len(self.node.get('/adapters')), 3)
850 self.assertEqual(self.log_levels().keys(), ['0', '1', '3'])
851
852 def test_mixed_add_remove_update_changes(self):
853 proxy = self.node.get_proxy('/')
854 tx1 = proxy.open_transaction()
855 self.make_change(tx1, '/adapters/2', 'config.log_level', 2)
856 tx1.remove('/adapters/0')
857 tx1.add('/adapters', Adapter(id='new'))
858 tx1.remove('/adapters/4')
859 tx1.add('/adapters', Adapter(id='new2'))
860 tx1.add('/adapters', Adapter(id='new3'))
861 self.assertEqual(len(proxy.get('/adapters')), 5)
862 self.assertEqual(len(self.node.get('/adapters')), 5)
863 self.assertEqual(len(tx1.get('/adapters')), 6)
864 tx1.commit()
865 self.assertEqual(len(proxy.get('/adapters')), 6)
866 self.assertEqual(len(self.node.get('/adapters')), 6)
867 self.assertEqual(self.log_levels(), {
868 '1': 3, '2': 2, '3': 3, 'new': 0, 'new2': 0, 'new3': 0
869 })
870
871 def test_compatible_updates(self):
872 proxy = self.node.get_proxy('/')
873 tx1 = proxy.open_transaction()
874 tx2 = proxy.open_transaction()
875 tx3 = proxy.open_transaction()
876 tx4 = proxy.open_transaction()
877 tx5 = proxy.open_transaction()
878 tx1.update('/health', HealthStatus(state=HealthStatus.OVERLOADED))
879 self.make_change(tx2, '/adapters/1', 'version', '42')
880 self.make_change(tx3, '/adapters/2', 'config.log_level', 2)
881 self.make_change(tx4, '/adapters/1', 'version', '42')
882 self.make_change(tx5, '/adapters/1', 'version', '422')
883 tx1.commit()
884 tx2.commit()
885 tx3.commit()
886 tx4.commit()
887 self.assertRaises(MergeConflictException, tx5.commit)
888
889 # verify outcome
890 self.assertEqual(self.node.get('/health').state, 1)
891 self.assertEqual(self.node.get('/', deep=1).adapters[1].version, '42')
892 self.assertEqual(self.log_levels(), {
893 '0': 3, '1': 3, '2': 2, '3': 3, '4': 3
894 })
895
896 def test_conflciting_updates(self):
897 proxy = self.node.get_proxy('/')
898 tx1 = proxy.open_transaction()
899 tx2 = proxy.open_transaction()
900 tx3 = proxy.open_transaction()
901 tx1.update('/health', HealthStatus(state=HealthStatus.OVERLOADED))
902 self.make_change(tx2, '/adapters/1', 'version', '42')
903 self.make_change(tx3, '/adapters/1', 'config.log_level', 2)
904 tx1.commit()
905 tx2.commit()
906 self.assertRaises(MergeConflictException, tx3.commit)
907
908 # verify outcome
909 self.assertEqual(self.node.get('/health').state, 1)
910 self.assertEqual(self.node.get('/', deep=1).adapters[1].version, '42')
911 self.assertEqual(self.log_levels(), {
912 '0': 3, '1': 3, '2': 3, '3': 3, '4': 3
913 })
914
915 def test_compatible_adds(self):
916 proxy = self.node.get_proxy('/')
917 tx1 = proxy.open_transaction()
918 tx2 = proxy.open_transaction()
919 tx3 = proxy.open_transaction()
920 tx1.add('/adapters', Adapter(id='new1'))
921 tx2.add('/adapters', Adapter(id='new2'))
922 tx3.add('/adapters', Adapter(id='new3'))
923 tx1.commit()
924 tx2.commit()
925 tx3.commit()
926 self.assertEqual(self.log_levels().keys(), [
927 '0', '1', '2', '3', '4', 'new1', 'new2', 'new3'
928 ])
929
930 def test_colliding_adds(self):
931 proxy = self.node.get_proxy('/')
932 tx1 = proxy.open_transaction()
933 tx2 = proxy.open_transaction()
934 tx3 = proxy.open_transaction()
935 tx4 = proxy.open_transaction()
936 tx1.add('/adapters', Adapter(id='new1'))
937 tx2.add('/adapters', Adapter(id='new2'))
938 tx3.add('/adapters', Adapter(id='new1', version='foobar'))
939 tx4.add('/adapters', Adapter(id='new1'))
940 tx1.commit()
941 tx2.commit()
942 self.assertRaises(MergeConflictException, tx3.commit)
943 tx4.commit() # is fine since it added the same data
944 self.assertEqual(self.log_levels().keys(), [
945 '0', '1', '2', '3', '4', 'new1', 'new2'
946 ])
947
948 def test_compatible_removes(self):
949 # removes are always compatible with each other
950 proxy = self.node.get_proxy('/')
951 tx1 = proxy.open_transaction()
952 tx2 = proxy.open_transaction()
953 tx3 = proxy.open_transaction()
954 tx1.remove('/adapters/0')
955 tx2.remove('/adapters/3')
956 tx3.remove('/adapters/0')
957 tx1.commit()
958 tx2.commit()
959 tx3.commit()
960 self.assertEqual(self.log_levels().keys(), ['1', '2', '4'])
961
962 def test_update_remove_conflict(self):
963 proxy = self.node.get_proxy('/')
964 tx1 = proxy.open_transaction()
965 tx2 = proxy.open_transaction()
966 tx3 = proxy.open_transaction()
967 self.make_change(tx1, '/adapters/0', 'version', '42')
968 tx1.remove('/adapters/1')
969 self.make_change(tx2, '/adapters/1', 'version', '13')
970 tx3.remove('/adapters/0')
971 tx1.commit()
972 self.assertRaises(MergeConflictException, tx2.commit)
973 self.assertRaises(MergeConflictException, tx3.commit)
974 self.assertEqual(self.log_levels().keys(), ['0', '2', '3', '4'])
975
976 def test_compatible_update_remove_mix(self):
977 proxy = self.node.get_proxy('/')
978 tx1 = proxy.open_transaction()
979 tx2 = proxy.open_transaction()
980 tx3 = proxy.open_transaction()
981 self.make_change(tx1, '/adapters/0', 'version', '42')
982 tx1.remove('/adapters/1')
983 self.make_change(tx2, '/adapters/2', 'version', '13')
984 tx3.remove('/adapters/3')
985 tx1.commit()
986 tx2.commit()
987 tx3.commit()
988 self.assertEqual(self.log_levels().keys(), ['0', '2', '4'])
989
990 def test_update_add_mix(self):
991 # at same nodes updates are always compatible with adds
992 proxy = self.node.get_proxy('/')
993 tx1 = proxy.open_transaction()
994 tx2 = proxy.open_transaction()
995 tx3 = proxy.open_transaction()
996 self.make_change(tx1, '/adapters/0', 'config.log_level', 4)
997 self.make_change(tx1, '/adapters/2', 'config.log_level', 4)
998 tx2.add('/adapters', Adapter(id='new1'))
999 tx3.add('/adapters', Adapter(id='new2'))
1000 tx1.commit()
1001 tx2.commit()
1002 tx3.commit()
1003 self.assertEqual(self.log_levels().keys(), [
1004 '0', '1', '2', '3', '4', 'new1', 'new2'
1005 ])
1006
1007 def test_remove_add_mix(self):
1008 # at same node, adds are always compatible with removes
1009 proxy = self.node.get_proxy('/')
1010 tx1 = proxy.open_transaction()
1011 tx2 = proxy.open_transaction()
1012 tx3 = proxy.open_transaction()
1013 tx1.remove('/adapters/0')
1014 tx2.add('/adapters', Adapter(id='new1'))
1015 tx3.add('/adapters', Adapter(id='new2'))
1016 tx1.remove('/adapters/4')
1017 tx1.commit()
1018 tx2.commit()
1019 tx3.commit()
1020 self.assertEqual(self.log_levels().keys(), [
1021 '1', '2', '3', 'new1', 'new2'
1022 ])
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001023
1024 def make_complex_changes(self):
1025
1026 # Plan:
1027 # Have two root proxies and two proxies on specific adapters
1028 # Make several transactions, including conflicting ones
1029 # Check as much as possible in terms of expected operations
1030
1031 proxy1 = self.node.get_proxy('/')
1032 proxy2 = self.node.get_proxy('/')
1033 proxy3 = self.node.get_proxy('/adapters/0')
1034 proxy4 = self.node.get_proxy('/adapters/1')
1035
1036 tx1 = proxy1.open_transaction()
1037 tx2 = proxy1.open_transaction()
1038 tx3 = proxy2.open_transaction()
1039 tx4 = proxy3.open_transaction()
1040 tx5 = proxy4.open_transaction()
1041
1042 # Make multiple changes via tx1
1043 self.make_change(tx1, '/adapters/0', 'config.log_level', 1)
1044 tx1.add('/adapters', Adapter(id='new1'))
1045 tx1.remove('/adapters/2')
1046
1047 # Make a non-conflicting change from tx2
1048 self.make_change(tx2, '/adapters/3', 'config.log_level', 0)
1049
1050 # Make some conflicting changes via tx3 now
1051 self.make_change(tx3, '/adapters/1', 'config.log_level', 1)
1052
1053 # Make some changes via leaf proxies
1054 my_adapter = tx4.get('/')
1055 my_adapter.version = 'zulu'
1056 my_adapter.config.log_level = 0
1057 tx4.update('/', my_adapter)
1058
1059 # Make some changes via leaf proxies
1060 my_adapter = tx5.get('/')
1061 my_adapter.version = 'brand new'
1062 my_adapter.config.log_level = 4
1063 tx5.update('/', my_adapter)
1064
1065 # Make some more changes on tx2
1066 tx2.add('/adapters', Adapter(id='new2'))
1067
1068 # Conflicts:
1069 # - tx4 conflicts with tx0
1070 # - tx5 conflicts with tx3
1071 return tx1, tx2, tx3, tx4, tx5
1072
Matt Jeanneret58f7a552018-10-02 07:00:27 -04001073 def test_complex_changes_seq1(self):
1074 tx1, tx2, tx3, tx4, tx5 = self.make_complex_changes()
1075 tx1.commit()
1076 tx2.commit()
1077 tx3.commit()
1078 self.assertRaises(MergeConflictException, tx4.commit)
1079 self.assertRaises(MergeConflictException, tx5.commit)
1080 self.assertEqual(self.log_levels(), {
1081 '0': 1, '1': 1, '3': 0, '4': 3, 'new1': 0, 'new2': 0
1082 })
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001083
Matt Jeanneret58f7a552018-10-02 07:00:27 -04001084 def test_complex_changes_seq2(self):
1085 tx1, tx2, tx3, tx4, tx5 = self.make_complex_changes()
1086 tx5.commit()
1087 tx4.commit()
1088 self.assertRaises(MergeConflictException, tx3.commit)
1089 tx2.commit()
1090 self.assertRaises(MergeConflictException, tx1.commit)
1091 self.assertEqual(self.log_levels(), {
1092 '0': 0, '1': 4, '2': 3, '3': 0, '4': 3, 'new2': 0
1093 })
1094
1095 def test_complex_changes_seq3(self):
1096 tx1, tx2, tx3, tx4, tx5 = self.make_complex_changes()
1097 tx4.commit()
1098 tx3.commit()
1099 tx2.commit()
1100 self.assertRaises(MergeConflictException, tx1.commit)
1101 self.assertRaises(MergeConflictException, tx5.commit)
1102 self.assertEqual(self.log_levels(), {
1103 '0': 0, '1': 1, '2': 3, '3': 0, '4': 3, 'new2': 0
1104 })
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001105
1106 def test_canceling_adds(self):
1107 proxy = self.node.get_proxy('/')
1108 tx = proxy.open_transaction()
1109 tx.add('/adapters', Adapter(id='new'))
1110 tx.add('/adapters', Adapter(id='new2'))
1111 tx.cancel()
1112 self.assertEqual(self.log_levels().keys(), ['0', '1', '2', '3', '4'])
1113
Matt Jeanneret58f7a552018-10-02 07:00:27 -04001114 def test_nested_adds(self):
1115 self.node.add('/logical_devices', LogicalDevice(id='0'))
1116 self.node.add('/logical_devices', LogicalDevice(id='1'))
1117 proxy0 = self.node.get_proxy('/logical_devices/0')
1118 proxy1 = self.node.get_proxy('/logical_devices/1')
1119 tx0 = proxy0.open_transaction()
1120 tx1 = proxy1.open_transaction()
1121
1122 tx0.add('/ports', LogicalPort(
1123 id='0', ofp_port=ofp_port(port_no=0, name='/0')))
1124 tx0.add('/ports', LogicalPort(
1125 id='1', ofp_port=ofp_port(port_no=1, name='/1')))
1126 tx1.add('/ports', LogicalPort(
1127 id='2', ofp_port=ofp_port(port_no=0, name='/0')))
1128
1129 # at this point none of these are visible outside of tx
1130 self.assertEqual(len(proxy0.get('/', deep=1).ports), 0)
1131 self.assertEqual(len(proxy1.get('/', deep=1).ports), 0)
1132
1133 tx0.commit()
1134 self.assertEqual(len(proxy0.get('/', deep=1).ports), 2)
1135 self.assertEqual(len(proxy1.get('/', deep=1).ports), 0)
1136
1137 tx1.commit()
1138 self.assertEqual(len(proxy0.get('/', deep=1).ports), 2)
1139 self.assertEqual(len(proxy1.get('/', deep=1).ports), 1)
1140
1141 def test_nested_removes(self):
1142 self.node.add('/logical_devices', LogicalDevice(id='0'))
1143 proxy0 = self.node.get_proxy('/logical_devices/0')
1144
1145 # add some ports to a device
1146 tx0 = proxy0.open_transaction()
1147 for i in xrange(10):
1148 tx0.add('/ports', LogicalPort(
1149 id=str(i), ofp_port=ofp_port(port_no=i, name='/{}'.format(i))))
1150 # self.assertRaises(ValueError, tx0.add, '/ports', LogicalPort(id='1'))
1151 tx0.commit()
1152
1153 # now to the removal
1154
1155 tx0 = proxy0.open_transaction()
1156 tx0.remove('/ports/0')
1157 tx0.remove('/ports/5')
1158
1159 tx1 = proxy0.open_transaction()
1160 tx1.remove('/ports/2')
1161 tx1.remove('/ports/7')
1162
1163 tx0.commit()
1164 tx1.commit()
1165
1166 port_ids = [
1167 p.ofp_port.port_no for p
1168 in self.node.get(deep=1).logical_devices[0].ports
1169 ]
1170 self.assertEqual(port_ids, [1, 3, 4, 6, 8, 9])
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001171
1172 # TODO need more tests to hammer out potential issues with transactions \
1173 # on nested nodes
1174
Matt Jeanneret58f7a552018-10-02 07:00:27 -04001175 def test_transactions_defer_post_op_callbacks(self):
1176
1177 proxy = self.node.get_proxy('/')
1178
1179 pre_update = Mock()
1180 post_update = Mock()
1181 pre_add = Mock()
1182 post_add = Mock()
1183 pre_remove = Mock()
1184 post_remove = Mock()
1185
1186 proxy.register_callback(CallbackType.PRE_UPDATE, pre_update)
1187 proxy.register_callback(CallbackType.POST_UPDATE, post_update)
1188 proxy.register_callback(CallbackType.PRE_ADD, pre_add)
1189 proxy.register_callback(CallbackType.POST_ADD, post_add)
1190 proxy.register_callback(CallbackType.PRE_REMOVE, pre_remove)
1191 proxy.register_callback(CallbackType.POST_REMOVE, post_remove)
1192
1193 tx = proxy.open_transaction()
1194
1195 # make some changes of each type
1196 v = tx.get('/')
1197 v.version = '42'
1198 tx.update('/', v)
1199 ad = tx.get('/adapters/1')
1200 tx.remove('/adapters/1')
1201 ld = LogicalDevice(id='1')
1202 tx.add('/logical_devices', ld)
1203
1204 # each pre_* should have been called exactly once, but none of the
1205 # post_* callbacks have been called yet
1206 pre_update.assert_called_once_with(v)
1207 pre_add.assert_called_once_with(ld)
1208 pre_remove.assert_called_once_with(ad)
1209 post_update.assert_not_called()
1210 post_add.assert_not_called()
1211 post_remove.assert_not_called()
1212
1213 # once we commit, we shall get the other callbacks
1214 tx.commit()
1215 post_update.assert_called_once_with(v)
1216 post_add.assert_called_once_with(ld)
1217 # OperationContext(
1218 # data=ld,
1219 # field_name='logical_devices',
1220 # child_key='1'
1221 # ))
1222 post_remove.assert_called_once_with(ad)
1223
Zsolt Harasztidafefe12016-11-14 21:29:58 -08001224
1225if __name__ == '__main__':
1226 main()