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