1# Copyright (c) 2017 Cisco and/or its affiliates. 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17# 18 19from __future__ import (absolute_import, division, print_function) 20 21import json 22 23from units.compat.mock import patch 24from units.compat import unittest 25from ansible.module_utils.network.nso import nso 26 27 28MODULE_PREFIX_MAP = ''' 29{ 30 "ansible-nso": "an", 31 "test": "test", 32 "tailf-ncs": "ncs" 33} 34''' 35 36 37SCHEMA_DATA = { 38 '/an:id-name-leaf': ''' 39{ 40 "meta": { 41 "prefix": "an", 42 "namespace": "http://github.com/ansible/nso", 43 "types": { 44 "http://github.com/ansible/nso:id-name-t": [ 45 { 46 "name": "http://github.com/ansible/nso:id-name-t", 47 "enumeration": [ 48 { 49 "label": "id-one" 50 }, 51 { 52 "label": "id-two" 53 } 54 ] 55 }, 56 { 57 "name": "identityref" 58 } 59 ] 60 }, 61 "keypath": "/an:id-name-leaf" 62 }, 63 "data": { 64 "kind": "leaf", 65 "type": { 66 "namespace": "http://github.com/ansible/nso", 67 "name": "id-name-t" 68 }, 69 "name": "id-name-leaf", 70 "qname": "an:id-name-leaf" 71 } 72}''', 73 '/an:id-name-values': ''' 74{ 75 "meta": { 76 "prefix": "an", 77 "namespace": "http://github.com/ansible/nso", 78 "types": {}, 79 "keypath": "/an:id-name-values" 80 }, 81 "data": { 82 "kind": "container", 83 "name": "id-name-values", 84 "qname": "an:id-name-values", 85 "children": [ 86 { 87 "kind": "list", 88 "name": "id-name-value", 89 "qname": "an:id-name-value", 90 "key": [ 91 "name" 92 ] 93 } 94 ] 95 } 96} 97''', 98 '/an:id-name-values/id-name-value': ''' 99{ 100 "meta": { 101 "prefix": "an", 102 "namespace": "http://github.com/ansible/nso", 103 "types": { 104 "http://github.com/ansible/nso:id-name-t": [ 105 { 106 "name": "http://github.com/ansible/nso:id-name-t", 107 "enumeration": [ 108 { 109 "label": "id-one" 110 }, 111 { 112 "label": "id-two" 113 } 114 ] 115 }, 116 { 117 "name": "identityref" 118 } 119 ] 120 }, 121 "keypath": "/an:id-name-values/id-name-value" 122 }, 123 "data": { 124 "kind": "list", 125 "name": "id-name-value", 126 "qname": "an:id-name-value", 127 "key": [ 128 "name" 129 ], 130 "children": [ 131 { 132 "kind": "key", 133 "name": "name", 134 "qname": "an:name", 135 "type": { 136 "namespace": "http://github.com/ansible/nso", 137 "name": "id-name-t" 138 } 139 }, 140 { 141 "kind": "leaf", 142 "type": { 143 "primitive": true, 144 "name": "string" 145 }, 146 "name": "value", 147 "qname": "an:value" 148 } 149 ] 150 } 151} 152''', 153 '/test:test': ''' 154{ 155 "meta": { 156 "types": { 157 "http://example.com/test:t15": [ 158 { 159 "leaf_type":[ 160 { 161 "name":"string" 162 } 163 ], 164 "list_type":[ 165 { 166 "name":"http://example.com/test:t15", 167 "leaf-list":true 168 } 169 ] 170 } 171 ] 172 } 173 }, 174 "data": { 175 "kind": "list", 176 "name":"test", 177 "qname":"test:test", 178 "key":["name"], 179 "children": [ 180 { 181 "kind": "key", 182 "name": "name", 183 "qname": "test:name", 184 "type": {"name":"string","primitive":true} 185 }, 186 { 187 "kind": "choice", 188 "name": "test-choice", 189 "qname": "test:test-choice", 190 "cases": [ 191 { 192 "kind": "case", 193 "name": "direct-child-case", 194 "qname":"test:direct-child-case", 195 "children":[ 196 { 197 "kind": "leaf", 198 "name": "direct-child", 199 "qname": "test:direct-child", 200 "type": {"name":"string","primitive":true} 201 } 202 ] 203 }, 204 { 205 "kind":"case","name":"nested-child-case","qname":"test:nested-child-case", 206 "children": [ 207 { 208 "kind": "choice", 209 "name": "nested-choice", 210 "qname": "test:nested-choice", 211 "cases": [ 212 { 213 "kind":"case","name":"nested-child","qname":"test:nested-child", 214 "children": [ 215 { 216 "kind": "leaf", 217 "name":"nested-child", 218 "qname":"test:nested-child", 219 "type":{"name":"string","primitive":true}} 220 ] 221 } 222 ] 223 } 224 ] 225 } 226 ] 227 }, 228 { 229 "kind":"leaf-list", 230 "name":"device-list", 231 "qname":"test:device-list", 232 "type": { 233 "namespace":"http://example.com/test", 234 "name":"t15" 235 } 236 } 237 ] 238 } 239} 240''', 241 '/test:test/device-list': ''' 242{ 243 "meta": { 244 "types": { 245 "http://example.com/test:t15": [ 246 { 247 "leaf_type":[ 248 { 249 "name":"string" 250 } 251 ], 252 "list_type":[ 253 { 254 "name":"http://example.com/test:t15", 255 "leaf-list":true 256 } 257 ] 258 } 259 ] 260 } 261 }, 262 "data": { 263 "kind":"leaf-list", 264 "name":"device-list", 265 "qname":"test:device-list", 266 "type": { 267 "namespace":"http://example.com/test", 268 "name":"t15" 269 } 270 } 271} 272''', 273 '/test:deps': ''' 274{ 275 "meta": { 276 }, 277 "data": { 278 "kind":"container", 279 "name":"deps", 280 "qname":"test:deps", 281 "children": [ 282 { 283 "kind": "leaf", 284 "type": { 285 "primitive": true, 286 "name": "string" 287 }, 288 "name": "a", 289 "qname": "test:a", 290 "deps": ["/test:deps/c"] 291 }, 292 { 293 "kind": "leaf", 294 "type": { 295 "primitive": true, 296 "name": "string" 297 }, 298 "name": "b", 299 "qname": "test:b", 300 "deps": ["/test:deps/a"] 301 }, 302 { 303 "kind": "leaf", 304 "type": { 305 "primitive": true, 306 "name": "string" 307 }, 308 "name": "c", 309 "qname": "test:c" 310 } 311 ] 312 } 313} 314''' 315} 316 317 318class MockResponse(object): 319 def __init__(self, method, params, code, body, headers=None): 320 if headers is None: 321 headers = {} 322 323 self.method = method 324 self.params = params 325 326 self.code = code 327 self.body = body 328 self.headers = dict(headers) 329 330 def read(self): 331 return self.body 332 333 334def mock_call(calls, url, timeout, validate_certs, data=None, headers=None, method=None): 335 result = calls[0] 336 del calls[0] 337 338 request = json.loads(data) 339 if result.method != request['method']: 340 raise ValueError('expected method {0}({1}), got {2}({3})'.format( 341 result.method, result.params, 342 request['method'], request['params'])) 343 344 for key, value in result.params.items(): 345 if key not in request['params']: 346 raise ValueError('{0} not in parameters'.format(key)) 347 if value != request['params'][key]: 348 raise ValueError('expected {0} to be {1}, got {2}'.format( 349 key, value, request['params'][key])) 350 351 return result 352 353 354def get_schema_response(path): 355 return MockResponse( 356 'get_schema', {'path': path}, 200, '{{"result": {0}}}'.format( 357 SCHEMA_DATA[path])) 358 359 360class TestJsonRpc(unittest.TestCase): 361 @patch('ansible.module_utils.network.nso.nso.open_url') 362 def test_exists(self, open_url_mock): 363 calls = [ 364 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 365 MockResponse('exists', {'path': '/exists'}, 200, '{"result": {"exists": true}}'), 366 MockResponse('exists', {'path': '/not-exists'}, 200, '{"result": {"exists": false}}') 367 ] 368 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 369 client = nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False) 370 self.assertEquals(True, client.exists('/exists')) 371 self.assertEquals(False, client.exists('/not-exists')) 372 373 self.assertEqual(0, len(calls)) 374 375 @patch('ansible.module_utils.network.nso.nso.open_url') 376 def test_exists_data_not_found(self, open_url_mock): 377 calls = [ 378 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 379 MockResponse('exists', {'path': '/list{missing-parent}/list{child}'}, 200, '{"error":{"type":"data.not_found"}}') 380 ] 381 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 382 client = nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False) 383 self.assertEquals(False, client.exists('/list{missing-parent}/list{child}')) 384 385 self.assertEqual(0, len(calls)) 386 387 388class TestValueBuilder(unittest.TestCase): 389 @patch('ansible.module_utils.network.nso.nso.open_url') 390 def test_identityref_leaf(self, open_url_mock): 391 calls = [ 392 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 393 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 394 get_schema_response('/an:id-name-leaf'), 395 MockResponse('get_module_prefix_map', {}, 200, '{{"result": {0}}}'.format(MODULE_PREFIX_MAP)) 396 ] 397 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 398 399 parent = "/an:id-name-leaf" 400 schema_data = json.loads( 401 SCHEMA_DATA['/an:id-name-leaf']) 402 schema = schema_data['data'] 403 404 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 405 vb.build(parent, None, 'ansible-nso:id-two', schema) 406 values = list(vb.values) 407 self.assertEquals(1, len(values)) 408 value = values[0] 409 self.assertEquals(parent, value.path) 410 self.assertEquals('set', value.state) 411 self.assertEquals('an:id-two', value.value) 412 413 self.assertEqual(0, len(calls)) 414 415 @patch('ansible.module_utils.network.nso.nso.open_url') 416 def test_identityref_key(self, open_url_mock): 417 calls = [ 418 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 419 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 420 get_schema_response('/an:id-name-values/id-name-value'), 421 MockResponse('get_module_prefix_map', {}, 200, '{{"result": {0}}}'.format(MODULE_PREFIX_MAP)), 422 MockResponse('exists', {'path': '/an:id-name-values/id-name-value{an:id-one}'}, 200, '{"result": {"exists": true}}') 423 ] 424 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 425 426 parent = "/an:id-name-values" 427 schema_data = json.loads( 428 SCHEMA_DATA['/an:id-name-values/id-name-value']) 429 schema = schema_data['data'] 430 431 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 432 vb.build(parent, 'id-name-value', [{'name': 'ansible-nso:id-one', 'value': '1'}], schema) 433 values = list(vb.values) 434 self.assertEquals(1, len(values)) 435 value = values[0] 436 self.assertEquals('{0}/id-name-value{{an:id-one}}/value'.format(parent), value.path) 437 self.assertEquals('set', value.state) 438 self.assertEquals('1', value.value) 439 440 self.assertEqual(0, len(calls)) 441 442 @patch('ansible.module_utils.network.nso.nso.open_url') 443 def test_nested_choice(self, open_url_mock): 444 calls = [ 445 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 446 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 447 get_schema_response('/test:test'), 448 MockResponse('exists', {'path': '/test:test{direct}'}, 200, '{"result": {"exists": true}}'), 449 MockResponse('exists', {'path': '/test:test{nested}'}, 200, '{"result": {"exists": true}}') 450 ] 451 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 452 453 parent = "/test:test" 454 schema_data = json.loads( 455 SCHEMA_DATA['/test:test']) 456 schema = schema_data['data'] 457 458 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 459 vb.build(parent, None, [{'name': 'direct', 'direct-child': 'direct-value'}, 460 {'name': 'nested', 'nested-child': 'nested-value'}], schema) 461 values = list(vb.values) 462 self.assertEquals(2, len(values)) 463 value = values[0] 464 self.assertEquals('{0}{{direct}}/direct-child'.format(parent), value.path) 465 self.assertEquals('set', value.state) 466 self.assertEquals('direct-value', value.value) 467 468 value = values[1] 469 self.assertEquals('{0}{{nested}}/nested-child'.format(parent), value.path) 470 self.assertEquals('set', value.state) 471 self.assertEquals('nested-value', value.value) 472 473 self.assertEqual(0, len(calls)) 474 475 @patch('ansible.module_utils.network.nso.nso.open_url') 476 def test_leaf_list_type(self, open_url_mock): 477 calls = [ 478 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.4"}'), 479 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 480 get_schema_response('/test:test') 481 ] 482 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 483 484 parent = "/test:test" 485 schema_data = json.loads( 486 SCHEMA_DATA['/test:test']) 487 schema = schema_data['data'] 488 489 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 490 vb.build(parent, None, {'device-list': ['one', 'two']}, schema) 491 values = list(vb.values) 492 self.assertEquals(1, len(values)) 493 value = values[0] 494 self.assertEquals('{0}/device-list'.format(parent), value.path) 495 self.assertEquals(['one', 'two'], value.value) 496 497 self.assertEqual(0, len(calls)) 498 499 @patch('ansible.module_utils.network.nso.nso.open_url') 500 def test_leaf_list_type_45(self, open_url_mock): 501 calls = [ 502 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 503 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 504 get_schema_response('/test:test/device-list') 505 ] 506 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 507 508 parent = "/test:test" 509 schema_data = json.loads( 510 SCHEMA_DATA['/test:test']) 511 schema = schema_data['data'] 512 513 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 514 vb.build(parent, None, {'device-list': ['one', 'two']}, schema) 515 values = list(vb.values) 516 self.assertEquals(3, len(values)) 517 value = values[0] 518 self.assertEquals('{0}/device-list'.format(parent), value.path) 519 self.assertEquals(nso.State.ABSENT, value.state) 520 value = values[1] 521 self.assertEquals('{0}/device-list{{one}}'.format(parent), value.path) 522 self.assertEquals(nso.State.PRESENT, value.state) 523 value = values[2] 524 self.assertEquals('{0}/device-list{{two}}'.format(parent), value.path) 525 self.assertEquals(nso.State.PRESENT, value.state) 526 527 self.assertEqual(0, len(calls)) 528 529 @patch('ansible.module_utils.network.nso.nso.open_url') 530 def test_sort_by_deps(self, open_url_mock): 531 calls = [ 532 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 533 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 534 get_schema_response('/test:deps') 535 ] 536 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 537 538 parent = "/test:deps" 539 schema_data = json.loads( 540 SCHEMA_DATA['/test:deps']) 541 schema = schema_data['data'] 542 543 values = { 544 'a': '1', 545 'b': '2', 546 'c': '3', 547 } 548 549 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 550 vb.build(parent, None, values, schema) 551 values = list(vb.values) 552 self.assertEquals(3, len(values)) 553 value = values[0] 554 self.assertEquals('{0}/c'.format(parent), value.path) 555 self.assertEquals('3', value.value) 556 value = values[1] 557 self.assertEquals('{0}/a'.format(parent), value.path) 558 self.assertEquals('1', value.value) 559 value = values[2] 560 self.assertEquals('{0}/b'.format(parent), value.path) 561 self.assertEquals('2', value.value) 562 563 self.assertEqual(0, len(calls)) 564 565 @patch('ansible.module_utils.network.nso.nso.open_url') 566 def test_sort_by_deps_not_included(self, open_url_mock): 567 calls = [ 568 MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5"}'), 569 MockResponse('new_trans', {}, 200, '{"result": {"th": 1}}'), 570 get_schema_response('/test:deps') 571 ] 572 open_url_mock.side_effect = lambda *args, **kwargs: mock_call(calls, *args, **kwargs) 573 574 parent = "/test:deps" 575 schema_data = json.loads( 576 SCHEMA_DATA['/test:deps']) 577 schema = schema_data['data'] 578 579 values = { 580 'a': '1', 581 'b': '2' 582 } 583 584 vb = nso.ValueBuilder(nso.JsonRpc('http://localhost:8080/jsonrpc', 10, False)) 585 vb.build(parent, None, values, schema) 586 values = list(vb.values) 587 self.assertEquals(2, len(values)) 588 value = values[0] 589 self.assertEquals('{0}/a'.format(parent), value.path) 590 self.assertEquals('1', value.value) 591 value = values[1] 592 self.assertEquals('{0}/b'.format(parent), value.path) 593 self.assertEquals('2', value.value) 594 595 self.assertEqual(0, len(calls)) 596 597 598class TestVerifyVersion(unittest.TestCase): 599 def test_valid_versions(self): 600 self.assertTrue(nso.verify_version_str('5.0', [(4, 6), (4, 5, 1)])) 601 self.assertTrue(nso.verify_version_str('5.1.1', [(4, 6), (4, 5, 1)])) 602 self.assertTrue(nso.verify_version_str('5.1.1.2', [(4, 6), (4, 5, 1)])) 603 self.assertTrue(nso.verify_version_str('4.6', [(4, 6), (4, 5, 1)])) 604 self.assertTrue(nso.verify_version_str('4.6.2', [(4, 6), (4, 5, 1)])) 605 self.assertTrue(nso.verify_version_str('4.6.2.1', [(4, 6), (4, 5, 1)])) 606 self.assertTrue(nso.verify_version_str('4.5.1', [(4, 6), (4, 5, 1)])) 607 self.assertTrue(nso.verify_version_str('4.5.2', [(4, 6), (4, 5, 1)])) 608 self.assertTrue(nso.verify_version_str('4.5.1.2', [(4, 6), (4, 5, 1)])) 609 610 def test_invalid_versions(self): 611 self.assertFalse(nso.verify_version_str('4.4', [(4, 6), (4, 5, 1)])) 612 self.assertFalse(nso.verify_version_str('4.4.1', [(4, 6), (4, 5, 1)])) 613 self.assertFalse(nso.verify_version_str('4.4.1.2', [(4, 6), (4, 5, 1)])) 614 self.assertFalse(nso.verify_version_str('4.5.0', [(4, 6), (4, 5, 1)])) 615 616 617class TestValueSort(unittest.TestCase): 618 def test_sort_parent_depend(self): 619 values = [ 620 nso.ValueBuilder.Value('/test/list{entry}', '/test/list', 'CREATE', ['']), 621 nso.ValueBuilder.Value('/test/list{entry}/description', '/test/list/description', 'TEST', ['']), 622 nso.ValueBuilder.Value('/test/entry', '/test/entry', 'VALUE', ['/test/list', '/test/list/name']) 623 ] 624 625 result = [v.path for v in nso.ValueBuilder.sort_values(values)] 626 627 self.assertEquals(['/test/list{entry}', '/test/entry', '/test/list{entry}/description'], result) 628 629 def test_sort_break_direct_cycle(self): 630 values = [ 631 nso.ValueBuilder.Value('/test/a', '/test/a', 'VALUE', ['/test/c']), 632 nso.ValueBuilder.Value('/test/b', '/test/b', 'VALUE', ['/test/a']), 633 nso.ValueBuilder.Value('/test/c', '/test/c', 'VALUE', ['/test/a']) 634 ] 635 636 result = [v.path for v in nso.ValueBuilder.sort_values(values)] 637 638 self.assertEquals(['/test/a', '/test/b', '/test/c'], result) 639 640 def test_sort_break_indirect_cycle(self): 641 values = [ 642 nso.ValueBuilder.Value('/test/c', '/test/c', 'VALUE', ['/test/a']), 643 nso.ValueBuilder.Value('/test/a', '/test/a', 'VALUE', ['/test/b']), 644 nso.ValueBuilder.Value('/test/b', '/test/b', 'VALUE', ['/test/c']) 645 ] 646 647 result = [v.path for v in nso.ValueBuilder.sort_values(values)] 648 649 self.assertEquals(['/test/a', '/test/c', '/test/b'], result) 650 651 def test_sort_depend_on_self(self): 652 values = [ 653 nso.ValueBuilder.Value('/test/a', '/test/a', 'VALUE', ['/test/a']), 654 nso.ValueBuilder.Value('/test/b', '/test/b', 'VALUE', []) 655 ] 656 657 result = [v.path for v in nso.ValueBuilder.sort_values(values)] 658 659 self.assertEqual(['/test/a', '/test/b'], result) 660