netconf-pnp-simulator: make PYTHONPATH always globally defined

Add IT using ncclient and tox

Issue-ID: INT-1124
Change-Id: I560d4fd2468ac93f8ead36062b2e316821af8d07
Signed-off-by: ebo <eliezio.oliveira@est.tech>
diff --git a/test/mocks/netconf-pnp-simulator/docs/examples/mynetconf/docker-compose.yml b/test/mocks/netconf-pnp-simulator/docs/examples/mynetconf/docker-compose.yml
index 5d8ba5a..5d7c0af 100644
--- a/test/mocks/netconf-pnp-simulator/docs/examples/mynetconf/docker-compose.yml
+++ b/test/mocks/netconf-pnp-simulator/docs/examples/mynetconf/docker-compose.yml
@@ -2,7 +2,7 @@
 
 services:
   netopeer2:
-    image: nexus3.onap.org:10001/onap/integration/simulators/netconf-pnp-simulator:2.6.1
+    image: nexus3.onap.org:10001/onap/integration/simulators/netconf-pnp-simulator:2.6.2
     container_name: mynetconf
     restart: always
     ports:
diff --git a/test/mocks/netconf-pnp-simulator/engine/Dockerfile b/test/mocks/netconf-pnp-simulator/engine/Dockerfile
index 4266069..d4776a4 100644
--- a/test/mocks/netconf-pnp-simulator/engine/Dockerfile
+++ b/test/mocks/netconf-pnp-simulator/engine/Dockerfile
@@ -151,6 +151,7 @@
 COPY --from=build /opt/ /opt/
 
 ENV LD_LIBRARY_PATH=/opt/lib:/opt/lib64
+ENV PYTHONPATH=/opt/lib/python3.7/site-packages
 
 COPY config/ /config
 VOLUME /config
@@ -160,6 +161,9 @@
 
 ENV HOME=/home/netconf
 VOLUME $HOME/.local/share/virtualenvs
+# This is NOT a robust health check but it does help tox-docker to detect when
+# it can start the tests.
+HEALTHCHECK --interval=1s --start-period=2s --retries=10 CMD test -f /run/netopeer2-server.pid
 
 EXPOSE 830
 
diff --git a/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml b/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml
index cd982b9..72191ff 100644
--- a/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml
+++ b/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml
@@ -1 +1 @@
-tag: "2.6.1"
+tag: "2.6.2"
diff --git a/test/mocks/netconf-pnp-simulator/engine/entrypoint.sh b/test/mocks/netconf-pnp-simulator/engine/entrypoint.sh
index 48a5e5a..6636080 100755
--- a/test/mocks/netconf-pnp-simulator/engine/entrypoint.sh
+++ b/test/mocks/netconf-pnp-simulator/engine/entrypoint.sh
@@ -112,7 +112,7 @@
 command=$prog $model
 redirect_stderr=true
 autorestart=true
-environment=PATH=$PROG_PATH,PYTHONPATH=/opt/lib/python3.7/site-packages,PYTHONUNBUFFERED="1"
+environment=PATH=$PROG_PATH,PYTHONUNBUFFERED="1"
 EOF
 }
 
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/README b/test/mocks/netconf-pnp-simulator/engine/tests/README
new file mode 100644
index 0000000..295585d
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/README
@@ -0,0 +1,2 @@
+Borrowed from https://github.com/sysrepo/sysrepo-netopeer2-smoketests
+with some minor fixes
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py b/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py
new file mode 100644
index 0000000..2f848c3
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py
@@ -0,0 +1,37 @@
+from ncclient import manager, operations
+import settings
+import unittest
+
+class NCTestCase(unittest.TestCase):
+    """ Base class for NETCONF test cases. Provides a NETCONF connection and some helper methods. """
+
+    def setUp(self):
+        self.nc = manager.connect(
+            host=settings.HOST,
+            port=settings.PORT,
+            username=settings.USERNAME,
+            key_filename=settings.KEY_FILENAME,
+            allow_agent=False,
+            look_for_keys=False,
+            hostkey_verify=False)
+        self.nc.raise_mode = operations.RaiseMode.NONE
+
+    def tearDown(self):
+        self.nc.close_session()
+
+    def check_reply_ok(self, reply):
+        self.assertIsNotNone(reply)
+        if settings.DEBUG:
+            print(reply.xml)
+        self.assertTrue(reply.ok)
+        self.assertIsNone(reply.error)
+
+    def check_reply_err(self, reply):
+        self.assertIsNotNone(reply)
+        if settings.DEBUG:
+            print(reply.xml)
+        self.assertFalse(reply.ok)
+        self.assertIsNotNone(reply.error)
+
+    def check_reply_data(self, reply):
+        self.check_reply_ok(reply)
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/settings.py b/test/mocks/netconf-pnp-simulator/engine/tests/settings.py
new file mode 100644
index 0000000..749eb4c
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/settings.py
@@ -0,0 +1,11 @@
+import os
+
+HOST = "127.0.0.1"
+# Set by tox-docker
+# Unexpectedly, tox-docker uses the repository prefix instead of the image name to define the
+# variable prefix.
+PORT = int(os.environ["LOCALHOST_830_TCP_PORT"])
+USERNAME = "netconf"
+KEY_FILENAME = "../config/ssh/id_rsa"
+
+DEBUG = False
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py b/test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py
new file mode 100644
index 0000000..62d41c2
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py
@@ -0,0 +1,52 @@
+import unittest
+import nctest
+
+class TestBasicOperations(nctest.NCTestCase):
+    """ Tests basic NETCONF operations with no prerequisites on datastore content. """
+
+    def test_capabilities(self):
+        self.assertTrue(":startup" in self.nc.server_capabilities)
+        self.assertTrue(":candidate" in self.nc.server_capabilities)
+        self.assertTrue(":validate" in self.nc.server_capabilities)
+        self.assertTrue(":xpath" in self.nc.server_capabilities)
+
+    def test_get(self):
+        reply = self.nc.get()
+        self.check_reply_data(reply)
+
+    def test_get_config_startup(self):
+        reply = self.nc.get_config(source='startup')
+        self.check_reply_data(reply)
+
+    def test_get_config_running(self):
+        reply = self.nc.get_config(source='running')
+        self.check_reply_data(reply)
+
+    def test_copy_config(self):
+        reply = self.nc.copy_config(source='startup', target='candidate')
+        self.check_reply_ok(reply)
+
+    def test_neg_filter(self):
+        reply = self.nc.get(filter=("xpath", "/non-existing-module:non-existing-data"))
+        self.check_reply_err(reply)
+
+    def test_lock(self):
+        reply = self.nc.lock("startup")
+        self.check_reply_ok(reply)
+        reply = self.nc.lock("running")
+        self.check_reply_ok(reply)
+        reply = self.nc.lock("candidate")
+        self.check_reply_ok(reply)
+
+        reply = self.nc.lock("startup")
+        self.check_reply_err(reply)
+
+        reply = self.nc.unlock("startup")
+        self.check_reply_ok(reply)
+        reply = self.nc.unlock("running")
+        self.check_reply_ok(reply)
+        reply = self.nc.unlock("candidate")
+        self.check_reply_ok(reply)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/test_ietf_interfaces.py b/test/mocks/netconf-pnp-simulator/engine/tests/test_ietf_interfaces.py
new file mode 100644
index 0000000..87733ac
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/test_ietf_interfaces.py
@@ -0,0 +1,93 @@
+import unittest
+import nctest
+import os
+
+class TestIETFInterfaces(nctest.NCTestCase):
+    """ Tests basic NETCONF operations on the turing-machine YANG module. """
+
+    def __init__(self, *args, **kwargs):
+        super(TestIETFInterfaces, self).__init__(*args, **kwargs)
+        self.ns = {"nc": "urn:ietf:params:xml:ns:netconf:base:1.0", "if": "urn:ietf:params:xml:ns:yang:ietf-interfaces"}
+
+    def test_edit_config(self):
+        config_xml = """<nc:config xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
+            <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
+                <interface nc:operation="{}">
+                    <name>TestInterface</name>
+                    <description>Interface under test</description>
+                    <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
+                    <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
+                        <mtu>1500</mtu>
+                        <address>
+                            <ip>192.168.2.100</ip>
+                            <prefix-length>24</prefix-length>
+                        </address>
+                    </ipv4>
+                    <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
+                        <address>
+                            <ip>2001:db8::10</ip>
+                            <prefix-length>32</prefix-length>
+                        </address>
+                    </ipv6>
+                </interface>
+            </interfaces>
+        </nc:config>"""
+
+        filter_xml = """<nc:filter xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
+            <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces" />
+            </nc:filter>"""
+
+        with_default_report_all = """report-all"""
+
+        # get from running - should be empty
+        reply = self.nc.get_config(source="running", filter=filter_xml)
+        self.check_reply_data(reply)
+        deltas = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']", namespaces=self.ns)
+        self.assertEqual(len(deltas), 0)
+
+        # set data - candidate
+        reply = self.nc.edit_config(target='candidate', config=config_xml.format("merge"))
+        self.check_reply_ok(reply)
+
+        # get from candidate
+        reply = self.nc.get_config(source="candidate", filter=filter_xml)
+        self.check_reply_data(reply)
+        interfaces = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']", namespaces=self.ns)
+        self.assertEqual(len(interfaces), 1)
+
+        # default leaf should NOT be present
+        enabled = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']/enabled", namespaces=self.ns)
+        self.assertEqual(len(enabled), 0)
+
+        # get from candidate with with defaults = 'report-all'
+        reply = self.nc.get_config(source="candidate", filter=filter_xml, with_defaults=with_default_report_all)
+        self.check_reply_data(reply)
+        interfaces = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']", namespaces=self.ns)
+        self.assertEqual(len(interfaces), 1)
+
+        # default leaf should be present
+        enabled = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']/enabled", namespaces=self.ns)
+        self.assertEqual(len(enabled), 0) # TODO: change to 1 once this is implemented
+
+        # get from running - should be empty
+        reply = self.nc.get_config(source="running", filter=filter_xml)
+        self.check_reply_data(reply)
+        deltas = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']", namespaces=self.ns)
+        self.assertEqual(len(deltas), 0)
+
+        # commit - should fail, not enabled in running
+        reply = self.nc.commit()
+        self.check_reply_err(reply)
+
+        # delete from candidate
+        reply = self.nc.edit_config(target='candidate', config=config_xml.format("delete"))
+        self.check_reply_ok(reply)
+
+        # get from candidate - should be empty
+        reply = self.nc.get_config(source="candidate", filter=filter_xml)
+        self.check_reply_data(reply)
+        deltas = reply.data.xpath("/nc:rpc-reply/nc:data/if:interfaces/if:interface[if:name='TestInterface']", namespaces=self.ns)
+        self.assertEqual(len(deltas), 0)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/test_turing_machine.py b/test/mocks/netconf-pnp-simulator/engine/tests/test_turing_machine.py
new file mode 100644
index 0000000..63a0c2d
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/test_turing_machine.py
@@ -0,0 +1,124 @@
+import unittest
+import nctest
+import os
+
+class TestTuringMachine(nctest.NCTestCase):
+    """ Tests basic NETCONF operations on the turing-machine YANG module. """
+
+    def __init__(self, *args, **kwargs):
+        super(TestTuringMachine, self).__init__(*args, **kwargs)
+        self.ns = {"nc": "urn:ietf:params:xml:ns:netconf:base:1.0", "tm": "http://example.net/turing-machine"}
+
+    def check_deltas_in_data(self, data):
+        deltas = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/*", namespaces=self.ns)
+        self.assertNotEqual(len(deltas), 0)
+        for d in deltas:
+            self.assertTrue(d.tag.endswith("delta"))
+
+    def check_labels_only_in_data(self, data):
+        children = data.xpath("/nc:rpc-reply/nc:data/*", namespaces=self.ns)
+        self.assertNotEqual(len(children), 0)
+        for child in children:
+            self.assertTrue(child.tag.endswith("turing-machine"))
+        children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/*", namespaces=self.ns)
+        self.assertNotEqual(len(children), 0)
+        for child in children:
+            self.assertTrue(child.tag.endswith("transition-function"))
+        children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/*", namespaces=self.ns)
+        self.assertNotEqual(len(children), 0)
+        for child in children:
+            self.assertTrue(child.tag.endswith("delta"))
+        children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/tm:delta/*", namespaces=self.ns)
+        self.assertNotEqual(len(children), 0)
+        for child in children:
+            self.assertTrue(child.tag.endswith("label"))
+
+    def test_get(self):
+        reply = self.nc.get()
+        self.check_reply_data(reply)
+        self.check_deltas_in_data(reply.data)
+
+    def test_get_config_startup(self):
+        reply = self.nc.get_config(source="startup")
+        self.check_reply_data(reply)
+        self.check_deltas_in_data(reply.data)
+
+    def test_get_config_running(self):
+        reply = self.nc.get_config(source="running")
+        self.check_reply_data(reply)
+        self.check_deltas_in_data(reply.data)
+
+    def test_get_subtree_filter(self):
+        filter_xml = """<nc:filter xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
+            <turing-machine xmlns="http://example.net/turing-machine">
+                <transition-function>
+                    <delta>
+                        <label />
+                    </delta>
+                </transition-function>
+            </turing-machine>
+            </nc:filter>"""
+        reply = self.nc.get_config(source="running", filter=filter_xml)
+        self.check_reply_data(reply)
+        self.check_deltas_in_data(reply.data)
+        self.check_labels_only_in_data(reply.data)
+
+    def test_get_xpath_filter(self):
+        # https://github.com/ncclient/ncclient/issues/166
+        filter_xml = """<nc:filter type="xpath" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
+            xmlns:tm="http://example.net/turing-machine"
+            select="/tm:turing-machine/transition-function/delta/label" />
+            """
+        reply = self.nc.get(filter=filter_xml)
+        self.check_reply_data(reply)
+        self.check_deltas_in_data(reply.data)
+        self.check_labels_only_in_data(reply.data)
+
+    @unittest.skipIf(os.environ.get("DOCKER_IMG_TAG") == "latest", "bug in Netopeer2 replace operation")
+    def test_edit_config(self):
+        config_xml = """<nc:config xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0">
+            <turing-machine xmlns="http://example.net/turing-machine">
+                <transition-function>
+                    <delta nc:operation="{}">
+                        <label>test-transition-rule</label>
+                        <input>
+                            <symbol>{}</symbol>
+                            <state>{}</state>
+                        </input>
+                    </delta>
+                </transition-function>
+            </turing-machine></nc:config>"""
+        # merge
+        reply = self.nc.edit_config(target='running', config=config_xml.format("merge", 9, 99))
+        self.check_reply_ok(reply)
+        # get
+        reply = self.nc.get_config(source="running")
+        self.check_reply_data(reply)
+        deltas = reply.data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/tm:delta[tm:label='test-transition-rule']", namespaces=self.ns)
+        self.assertEqual(len(deltas), 1)
+        # create already existing - expect error
+        reply = self.nc.edit_config(target='running', config=config_xml.format("create", 9, 99))
+        self.check_reply_err(reply)
+        # replace
+        reply = self.nc.edit_config(target='running', config=config_xml.format("replace", 9, 88))
+        self.check_reply_ok(reply)
+        # get
+        reply = self.nc.get_config(source="running")
+        self.check_reply_data(reply)
+        states = reply.data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/tm:delta[tm:label='test-transition-rule']/tm:input/tm:state", namespaces=self.ns)
+        self.assertEqual(len(states), 1)
+        self.assertEqual(states[0].text, "88")
+        # delete
+        reply = self.nc.edit_config(target='running', config=config_xml.format("delete", 9, 88))
+        self.check_reply_ok(reply)
+        # delete non-existing - expect error
+        reply = self.nc.edit_config(target='running', config=config_xml.format("delete", 9, 88))
+        self.check_reply_err(reply)
+        # get - should be empty
+        reply = self.nc.get_config(source="running")
+        self.check_reply_data(reply)
+        deltas = reply.data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/tm:delta[tm:label='test-transition-rule']", namespaces=self.ns)
+        self.assertEqual(len(deltas), 0)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/mocks/netconf-pnp-simulator/engine/tox.ini b/test/mocks/netconf-pnp-simulator/engine/tox.ini
new file mode 100644
index 0000000..c4ca5fb
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tox.ini
@@ -0,0 +1,32 @@
+#-
+# ============LICENSE_START=======================================================
+#  Copyright (C) 2020 Nordix Foundation.
+# ================================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=========================================================
+
+[tox]
+requires = tox-docker
+skipsdist = True
+
+[testenv]
+changedir = tests
+docker =
+  {env:CONTAINER_PUSH_REGISTRY}/{env:DOCKER_NAME}:{env:DOCKER_IMAGE_TAG}
+
+deps =
+  ncclient
+  discover
+commands = discover -v
diff --git a/test/mocks/netconf-pnp-simulator/modules/docker-compose.yml b/test/mocks/netconf-pnp-simulator/modules/docker-compose.yml
index 8176e3b..e8f2f9a 100644
--- a/test/mocks/netconf-pnp-simulator/modules/docker-compose.yml
+++ b/test/mocks/netconf-pnp-simulator/modules/docker-compose.yml
@@ -2,7 +2,7 @@
 
 services:
   netconf-pnp-simulator:
-    image: nexus3.onap.org:10001/onap/integration/simulators/netconf-pnp-simulator:2.6.1
+    image: nexus3.onap.org:10001/onap/integration/simulators/netconf-pnp-simulator:2.6.2
     container_name: netconf-pnp-simulator
     restart: always
     ports: