aboutsummaryrefslogtreecommitdiffstats
path: root/netconfsimulator/netconf
diff options
context:
space:
mode:
authorBartosz Gardziejewski <bartosz.gardziejewski@nokia.com>2020-04-08 09:31:13 +0200
committerBogumil Zebek <bogumil.zebek@nokia.com>2020-04-08 09:43:31 +0000
commit3c494af52c476a86ae1389991b464914517774b8 (patch)
treee6d9b4f261eac5f7b3fd0f42e740840a106842e6 /netconfsimulator/netconf
parent75496bfc5b2f7e03e49ab4929d1f20962b39c992 (diff)
Move PNF simulator from /test/mocks to new project
This code is a copy of pnfsimulator located in integration repository (/test/mocks/pnfsimulator) with added profile "docker" in pom.xml located in pnfsimulator and netconfsimulator subprojects Issue-ID: INT-1517 Signed-off-by: Bartosz Gardziejewski <bartosz.gardziejewski@nokia.com> Change-Id: I725fa0530c41b13cb12705979dee8b8b354dc1a1
Diffstat (limited to 'netconfsimulator/netconf')
-rw-r--r--netconfsimulator/netconf/__init__.py19
-rwxr-xr-xnetconfsimulator/netconf/initialize_netopeer.sh37
-rw-r--r--netconfsimulator/netconf/load_server_certs.xml44
-rw-r--r--netconfsimulator/netconf/netopeer_change_saver.py107
-rw-r--r--netconfsimulator/netconf/newmodel.xml24
-rw-r--r--netconfsimulator/netconf/newmodel.yang9
-rw-r--r--netconfsimulator/netconf/pnf-simulator.data.xml24
-rw-r--r--netconfsimulator/netconf/pnf-simulator.yang9
-rwxr-xr-xnetconfsimulator/netconf/set-up-xmls.py162
-rw-r--r--netconfsimulator/netconf/test_yang_loader_server.py121
-rw-r--r--netconfsimulator/netconf/tls_listen.xml49
-rw-r--r--netconfsimulator/netconf/yang_loader.log1
-rw-r--r--netconfsimulator/netconf/yang_loader_server.py172
13 files changed, 778 insertions, 0 deletions
diff --git a/netconfsimulator/netconf/__init__.py b/netconfsimulator/netconf/__init__.py
new file mode 100644
index 0000000..aa8b4f9
--- /dev/null
+++ b/netconfsimulator/netconf/__init__.py
@@ -0,0 +1,19 @@
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
diff --git a/netconfsimulator/netconf/initialize_netopeer.sh b/netconfsimulator/netconf/initialize_netopeer.sh
new file mode 100755
index 0000000..59fc8a1
--- /dev/null
+++ b/netconfsimulator/netconf/initialize_netopeer.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
+
+cp /tls/* /usr/local/etc/keystored/keys/
+cp /netconf/*.xml /tmp/
+
+chmod +x /netconf/set-up-xmls.py
+/netconf/set-up-xmls.py /tls ca.crt server_cert.crt server_key.pem /tmp/load_server_certs.xml /tmp/tls_listen.xml client.crt
+
+/usr/bin/supervisord -c /etc/supervisord.conf &
+sysrepoctl --install --yang=/netconf/pnf-simulator.yang --owner=netconf:nogroup --permissions=777
+sysrepocfg --import=/netconf/pnf-simulator.data.xml --datastore=startup --format=xml --level=3 pnf-simulator
+sysrepocfg --merge=/tmp/load_server_certs.xml --format=xml --datastore=startup ietf-keystore
+sysrepocfg --merge=/tmp/tls_listen.xml --format=xml --datastore=startup ietf-netconf-server
+
+nohup python3 /netconf/yang_loader_server.py &
+
+python /netconf/netopeer_change_saver.py pnf-simulator kafka1:9092 config \ No newline at end of file
diff --git a/netconfsimulator/netconf/load_server_certs.xml b/netconfsimulator/netconf/load_server_certs.xml
new file mode 100644
index 0000000..b52f911
--- /dev/null
+++ b/netconfsimulator/netconf/load_server_certs.xml
@@ -0,0 +1,44 @@
+<!--
+ ============LICENSE_START=======================================================
+ Simulator
+ ================================================================================
+ Copyright (C) 2019 Nokia. All rights reserved.
+ ================================================================================
+ 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.
+ ============LICENSE_END=========================================================
+ -->
+
+<keystore xmlns="urn:ietf:params:xml:ns:yang:ietf-keystore">
+ <private-keys>
+ <private-key>
+ <name>SERVER_KEY_NAME</name>
+ <certificate-chains>
+ <certificate-chain>
+ <name>SERVER_CERT_NAME</name>
+ <certificate>SERVER_CERTIFICATE_HERE</certificate>
+ </certificate-chain>
+ </certificate-chains>
+ </private-key>
+ </private-keys>
+ <trusted-certificates>
+ <name>test_trusted_ca_list</name>
+ <trusted-certificate>
+ <name>CA_CERT_NAME</name>
+ <certificate>CA_CERTIFICATE_HERE</certificate>
+ </trusted-certificate>
+ <trusted-certificate>
+ <name>CLIENT_CERT_NAME</name>
+ <certificate>CLIENT_CERTIFICATE_HERE</certificate>
+ </trusted-certificate>
+ </trusted-certificates>
+</keystore>
diff --git a/netconfsimulator/netconf/netopeer_change_saver.py b/netconfsimulator/netconf/netopeer_change_saver.py
new file mode 100644
index 0000000..92f8846
--- /dev/null
+++ b/netconfsimulator/netconf/netopeer_change_saver.py
@@ -0,0 +1,107 @@
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
+
+import sysrepo as sr
+import sys
+import json
+import time
+import logging
+from kafka import KafkaProducer
+from enum import Enum
+
+logging.basicConfig(filename='netopeer_change_saver.log', level=logging.DEBUG)
+
+kafka_producer = None
+topic = "config"
+
+
+class OperationType(Enum):
+ CREATED = sr.SR_OP_CREATED
+ DELETED = sr.SR_OP_DELETED
+ MODIFIED = sr.SR_OP_MODIFIED
+ MOVED = sr.SR_OP_MOVED
+
+
+def module_change_callback(session, name, event, private_ctx):
+ if sr.SR_EV_APPLY == event:
+ change_path = "/{}:*".format(name)
+ changes = session.get_changes_iter(change_path)
+ change = session.get_change_next(changes)
+ while change:
+ try:
+ process_change(change)
+ change = session.get_change_next(changes)
+ except Exception:
+ logging.exception("Exception occured")
+
+ return sr.SR_ERR_OK
+
+
+def process_change(change):
+ if change:
+ message = {"type": OperationType(change.oper()).name}
+ if change.old_val():
+ message["old"] = {"path": change.old_val().xpath(), "value": change.old_val().val_to_string()}
+ if change.new_val():
+ message["new"] = {"path": change.new_val().xpath(), "value": change.new_val().val_to_string()}
+ send_message(message)
+
+
+def send_message(message):
+ logging.debug("Message to kafka : %s", message)
+ response = kafka_producer.send(topic, message)
+ logging.info(response.get(timeout=90))
+
+
+def create_producer(server):
+ for i in range(10): # pylint: disable=W0612
+ try:
+ return KafkaProducer(bootstrap_servers=server, value_serializer=lambda v: json.dumps(v).encode('utf-8'))
+ except Exception:
+ time.sleep(15)
+ raise Exception("Could not connect to kafka server")
+
+
+def print_current_config(kafka_session, module):
+ name = "/{}:*//*".format(module)
+ logging.info("Retrieving current config for %s module", name)
+ values = kafka_session.get_items(name)
+ for i in range(values.val_cnt()):
+ logging.info(values.val(i).to_string())
+
+
+if __name__ == "__main__":
+ try:
+ module_name = sys.argv[1]
+ bootstrap_servers = sys.argv[2]
+ topic = sys.argv[3]
+ connection = sr.Connection("example_application2")
+ session = sr.Session(connection)
+ subscribe = sr.Subscribe(session)
+ subscribe.module_change_subscribe(module_name, module_change_callback)
+
+ print_current_config(session, module_name)
+
+ kafka_producer = create_producer(bootstrap_servers)
+
+ sr.global_loop()
+ except Exception as e:
+ logging.exception("Exception occured")
+ raise e
diff --git a/netconfsimulator/netconf/newmodel.xml b/netconfsimulator/netconf/newmodel.xml
new file mode 100644
index 0000000..90a3451
--- /dev/null
+++ b/netconfsimulator/netconf/newmodel.xml
@@ -0,0 +1,24 @@
+<!--
+ ============LICENSE_START=======================================================
+ Simulator
+ ================================================================================
+ Copyright (C) 2019 Nokia. All rights reserved.
+ ================================================================================
+ 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.
+ ============LICENSE_END=========================================================
+ -->
+
+<config2 xmlns="http://onap.org/pnf-simulator2">
+ <item1>500</item1>
+ <item2>1000</item2>
+</config2>
diff --git a/netconfsimulator/netconf/newmodel.yang b/netconfsimulator/netconf/newmodel.yang
new file mode 100644
index 0000000..544f467
--- /dev/null
+++ b/netconfsimulator/netconf/newmodel.yang
@@ -0,0 +1,9 @@
+module newmodel {
+ namespace "http://onap.org/pnf-simulator2";
+ prefix config2;
+ container config2 {
+ config true;
+ leaf item1 {type uint32;}
+ leaf item2 {type uint32;}
+ }
+}
diff --git a/netconfsimulator/netconf/pnf-simulator.data.xml b/netconfsimulator/netconf/pnf-simulator.data.xml
new file mode 100644
index 0000000..c235f64
--- /dev/null
+++ b/netconfsimulator/netconf/pnf-simulator.data.xml
@@ -0,0 +1,24 @@
+<!--
+ ============LICENSE_START=======================================================
+ Simulator
+ ================================================================================
+ Copyright (C) 2019 Nokia. All rights reserved.
+ ================================================================================
+ 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.
+ ============LICENSE_END=========================================================
+ -->
+
+<config xmlns="http://onap.org/pnf-simulator">
+ <itemValue1>42</itemValue1>
+ <itemValue2>35</itemValue2>
+</config>
diff --git a/netconfsimulator/netconf/pnf-simulator.yang b/netconfsimulator/netconf/pnf-simulator.yang
new file mode 100644
index 0000000..ba11585
--- /dev/null
+++ b/netconfsimulator/netconf/pnf-simulator.yang
@@ -0,0 +1,9 @@
+module pnf-simulator {
+ namespace "http://onap.org/pnf-simulator";
+ prefix config;
+ container config {
+ config true;
+ leaf itemValue1 {type uint32;}
+ leaf itemValue2 {type uint32;}
+ }
+}
diff --git a/netconfsimulator/netconf/set-up-xmls.py b/netconfsimulator/netconf/set-up-xmls.py
new file mode 100755
index 0000000..2ec1cf2
--- /dev/null
+++ b/netconfsimulator/netconf/set-up-xmls.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python
+
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
+
+import os
+import sys
+import logging
+import logging.config
+
+logging.basicConfig()
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+# Placeholders definition - this needs to match placeholders in
+# load_server_certs_xml_file and tls_listen_xml_file
+SERVER_KEY_NAME = "SERVER_KEY_NAME"
+SERVER_CERT_NAME = "SERVER_CERT_NAME"
+SERVER_CERTIFICATE_HERE = "SERVER_CERTIFICATE_HERE"
+CA_CERT_NAME = "CA_CERT_NAME"
+CLIENT_CERT_NAME = "CLIENT_CERT_NAME"
+CLIENT_CERTIFICATE_HERE="CLIENT_CERTIFICATE_HERE"
+CA_CERTIFICATE_HERE = "CA_CERTIFICATE_HERE"
+CLIENT_FINGERPRINT_HERE = "CLIENT_FINGERPRINT_HERE"
+SERVER_CERTIFICATE_ENV = "SERVER_CERTIFICATE_ENV"
+CA_CERTIFICATE_ENV = "CA_CERTIFICATE_ENV"
+
+
+class FileHelper(object):
+ @classmethod
+ def get_file_contents(cls, filename):
+ with open(filename, "r") as f:
+ return f.read()
+
+ @classmethod
+ def write_file_contents(cls, filename, data):
+ with open(filename, "w+") as f:
+ f.write(data)
+
+
+class CertHelper(object):
+ @classmethod
+ def get_pem_content_stripped(cls, pem_dir, pem_filename):
+ cmd = "cat {}/{} | grep -v '^-'".format(pem_dir, pem_filename)
+ content = CertHelper.system(cmd)
+ return content
+
+ @classmethod
+ def get_cert_fingerprint(cls, directory, cert_filename):
+ cmd = "openssl x509 -fingerprint -noout -in {}/{} | sed -e " \
+ "'s/SHA1 Fingerprint//; s/=//; s/=//p'" \
+ .format(directory, cert_filename)
+ fingerprint = CertHelper.system(cmd)
+ return fingerprint
+
+ @classmethod
+ def print_certs_info(cls, ca_cert, ca_fingerprint, server_cert):
+ logger.info("Will use server certificate: " + server_cert)
+ logger.info("Will use CA certificate: " + ca_cert)
+ logger.info("CA certificate fingerprint: " + ca_fingerprint)
+
+ @classmethod
+ def system(cls, cmd):
+ return os.popen(cmd).read().replace("\n", "")
+
+
+class App(object):
+ @classmethod
+ def patch_server_certs(cls, data, server_key_filename_noext,
+ server_cert_filename_noext, ca_cert_filename_noext,
+ server_cert, ca_cert, client_cert_filename_noext, client_cert):
+ data = data.replace(SERVER_KEY_NAME, server_key_filename_noext)
+ data = data.replace(SERVER_CERT_NAME, server_cert_filename_noext)
+ data = data.replace(CA_CERT_NAME, ca_cert_filename_noext)
+ data = data.replace(CLIENT_CERT_NAME, client_cert_filename_noext)
+ data = data.replace(CLIENT_CERTIFICATE_HERE, client_cert)
+ data = data.replace(SERVER_CERTIFICATE_HERE, server_cert)
+ data = data.replace(CA_CERTIFICATE_HERE, ca_cert)
+ return data
+
+ @classmethod
+ def patch_tls_listen(cls, data, server_cert_filename_noext, client_fingerprint,
+ server_cert, ca_cert):
+ data = data.replace(SERVER_CERT_NAME, server_cert_filename_noext)
+ data = data.replace(CLIENT_FINGERPRINT_HERE, client_fingerprint)
+ data = data.replace(SERVER_CERTIFICATE_HERE, server_cert)
+ data = data.replace(CA_CERTIFICATE_HERE, ca_cert)
+ return data
+
+ @classmethod
+ def run(cls):
+ # name things
+ cert_dir = sys.argv[1]
+ ca_cert_filename = sys.argv[2]
+ server_cert_filename = sys.argv[3]
+ server_key_filename = sys.argv[4]
+ load_server_certs_xml_file = sys.argv[5]
+ tls_listen_xml_file = sys.argv[6]
+ client_cert_filename = sys.argv[7]
+
+
+ # strip extensions
+ ca_cert_filename_noext = ca_cert_filename.replace(".crt", "")
+ server_cert_filename_noext = server_cert_filename.replace(".crt", "")
+ server_key_filename_noext = server_key_filename.replace(".pem", "")
+ client_cert_filename_noext = client_cert_filename.replace(".crt", "")
+
+ # get certificates from files
+ server_cert = CertHelper.get_pem_content_stripped(cert_dir,
+ server_cert_filename)
+ ca_cert = CertHelper.get_pem_content_stripped(cert_dir,
+ ca_cert_filename)
+ client_fingerprint = CertHelper.get_cert_fingerprint(cert_dir,
+ client_cert_filename)
+ CertHelper.print_certs_info(ca_cert, client_fingerprint, server_cert)
+
+ client_cert = CertHelper.get_pem_content_stripped(cert_dir,
+ client_cert_filename)
+ # patch TLS configuration files
+ data_srv = FileHelper.get_file_contents(load_server_certs_xml_file)
+ patched_srv = App.patch_server_certs(data_srv, server_key_filename_noext,
+ server_cert_filename_noext,
+ ca_cert_filename_noext,
+ server_cert, ca_cert,
+ client_cert_filename_noext, client_cert)
+ FileHelper.write_file_contents(load_server_certs_xml_file, patched_srv)
+
+ data_tls = FileHelper.get_file_contents(tls_listen_xml_file)
+ patched_tls = App.patch_tls_listen(data_tls, server_cert_filename_noext,
+ client_fingerprint, server_cert, ca_cert)
+ FileHelper.write_file_contents(tls_listen_xml_file, patched_tls)
+
+
+def main():
+ if len(sys.argv) is not 8:
+ print("Usage: {1} <cert_dir> <ca_cert_filename> <server_cert_filename> "
+ "<server_key_filename> <load_server_certs_xml_full_path> "
+ "<tls_listen_full_path> <client_cert_filename>", sys.argv[0])
+ return 1
+ App.run()
+ logger.info("XML files patched successfully")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/netconfsimulator/netconf/test_yang_loader_server.py b/netconfsimulator/netconf/test_yang_loader_server.py
new file mode 100644
index 0000000..a222087
--- /dev/null
+++ b/netconfsimulator/netconf/test_yang_loader_server.py
@@ -0,0 +1,121 @@
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
+
+import unittest
+
+from unittest import mock
+from werkzeug.datastructures import FileStorage
+
+from yang_loader_server import YangLoaderHelper, YangModelServer
+
+
+class TestYangLoaderHelper(unittest.TestCase):
+
+ def test_should_save_file_and_return_path(self):
+ helper = YangLoaderHelper()
+ mocked_file = mock.Mock(FileStorage)
+ mocked_file.filename = "sample"
+
+ path = helper.save_file(mocked_file)
+
+ self.assertEqual(path, "/tmp/sample")
+ mocked_file.save.assert_called_once_with("/tmp/sample")
+
+ @mock.patch('yang_loader_server.check_output')
+ def test_should_install_new_yang_model(self, mocked_output):
+ helper = YangLoaderHelper()
+
+ helper.install_new_model("path")
+
+ mocked_output.assert_called_with(
+ ['sysrepoctl', '--install', '--yang=path',
+ '--owner=netconf:nogroup', '--permissions=777'],
+ stderr=-2, universal_newlines=True)
+
+ @mock.patch('yang_loader_server.check_output')
+ def test_should_delete_yang_model(self, mocked_output):
+ helper = YangLoaderHelper()
+
+ helper.uninstall_a_model("modelName")
+
+ mocked_output.assert_called_with(
+ ['sysrepoctl', '--uninstall', '--module=modelName'],
+ stderr=-2, universal_newlines=True)
+
+ @mock.patch('yang_loader_server.check_output')
+ def test_should_set_default_configuration(self, mocked_output):
+ helper = YangLoaderHelper()
+
+ helper.set_default_configuration("samplePath", "sampleModuleName")
+
+ mocked_output.assert_called_with(
+ ['sysrepocfg', '--import=samplePath', '--datastore=startup',
+ '--format=xml', '--level=3', 'sampleModuleName'],
+ stderr=-2, universal_newlines=True)
+
+ @mock.patch('yang_loader_server.subprocess.Popen')
+ @mock.patch('yang_loader_server.check_output')
+ def test_should_verify_change_listener_for_model_properly(self, mocked_output, mocked_popen):
+ helper = YangLoaderHelper()
+
+ helper.start_change_listener_for_model("sampleModule")
+
+ mocked_output.assert_called_with(
+ ['pgrep', '-f', 'python /netconf/netopeer_change_saver.py sampleModule kafka1:9092 config'],
+ stderr=-2, universal_newlines=True)
+
+ @mock.patch('yang_loader_server.check_output')
+ def test_should_raise_exception_when_error_occurred_in_output(self,
+ mocked_output):
+ helper = YangLoaderHelper()
+ mocked_output.return_value = "abcd ERR"
+ with self.assertRaises(RuntimeError) as context:
+ helper._run_bash_command("sample command")
+
+ self.assertEqual('abcd ERR', str(context.exception))
+
+
+class TestYangModelServer(unittest.TestCase):
+
+ def __init__(self, methodName='runTest'):
+ super().__init__(methodName)
+ self._mocked_file = mock.Mock(FileStorage)
+
+ def test_should_properly_apply_and_start_new_model(self):
+ with mock.patch.object(YangModelServer, '_parse_request',
+ new=self._mock_request):
+ helper = mock.Mock(YangLoaderHelper)
+ helper.save_file.return_value = "sampleFile"
+ server = YangModelServer(helper)
+
+ server.post()
+
+ self.assertEqual(helper.save_file.call_count, 2)
+ helper.install_new_model.assert_called_once_with('sampleFile')
+ helper.set_default_configuration.assert_called_once_with(
+ 'sampleFile', 'sampleModuleName')
+ helper.start_change_listener_for_model.assert_called_once_with('sampleModuleName')
+
+ def _mock_request(self):
+ return {
+ 'yangModel': self._mocked_file,
+ 'initialConfig': self._mocked_file,
+ 'moduleName': "sampleModuleName"
+ }
diff --git a/netconfsimulator/netconf/tls_listen.xml b/netconfsimulator/netconf/tls_listen.xml
new file mode 100644
index 0000000..4f610b5
--- /dev/null
+++ b/netconfsimulator/netconf/tls_listen.xml
@@ -0,0 +1,49 @@
+<!--
+ ============LICENSE_START=======================================================
+ Simulator
+ ================================================================================
+ Copyright (C) 2019 Nokia. All rights reserved.
+ ================================================================================
+ 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.
+ ============LICENSE_END=========================================================
+ -->
+
+<netconf-server xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-server">
+ <listen>
+ <endpoint>
+ <name>test_tls_listen_endpt</name>
+ <tls>
+ <address>0.0.0.0</address>
+ <port>6513</port>
+ <certificates>
+ <certificate>
+ <name>SERVER_CERT_NAME</name>
+ </certificate>
+ </certificates>
+ <client-auth>
+ <trusted-ca-certs>test_trusted_ca_list</trusted-ca-certs>
+ <trusted-client-certs>test_trusted_ca_list</trusted-client-certs>
+ <cert-maps>
+ <cert-to-name>
+ <id>1</id>
+ <!-- This is not a typo - 0x02 should stay there -->
+ <fingerprint>02:CLIENT_FINGERPRINT_HERE</fingerprint>
+ <map-type xmlns:x509c2n="urn:ietf:params:xml:ns:yang:ietf-x509-cert-to-name">x509c2n:specified</map-type>
+ <name>test</name>
+ </cert-to-name>
+ </cert-maps>
+ </client-auth>
+ </tls>
+ </endpoint>
+ </listen>
+</netconf-server>
diff --git a/netconfsimulator/netconf/yang_loader.log b/netconfsimulator/netconf/yang_loader.log
new file mode 100644
index 0000000..61b95b3
--- /dev/null
+++ b/netconfsimulator/netconf/yang_loader.log
@@ -0,0 +1 @@
+INFO:werkzeug: * Running on http://0.0.0.0:5002/ (Press CTRL+C to quit)
diff --git a/netconfsimulator/netconf/yang_loader_server.py b/netconfsimulator/netconf/yang_loader_server.py
new file mode 100644
index 0000000..27d46ce
--- /dev/null
+++ b/netconfsimulator/netconf/yang_loader_server.py
@@ -0,0 +1,172 @@
+###
+# ============LICENSE_START=======================================================
+# Simulator
+# ================================================================================
+# Copyright (C) 2019 Nokia. All rights reserved.
+# ================================================================================
+# 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.
+# ============LICENSE_END=========================================================
+###
+
+import logging
+import subprocess
+import os
+from subprocess import check_output, CalledProcessError
+from flask import Flask
+from flask_restful import Resource, Api, reqparse
+from werkzeug.datastructures import FileStorage
+import time
+
+app = Flask(__name__)
+api = Api(app)
+logger = logging.getLogger("yang-loader")
+logger.addHandler(logging.StreamHandler())
+KAFKA_BROKER_NAME="kafka1:9092"
+KAFKA_TOPIC_NAME="config"
+
+
+class YangLoaderHelper(object):
+
+ @classmethod
+ def save_file(cls, yang_model_file: FileStorage) -> str:
+ path = "/tmp/" + yang_model_file.filename
+ yang_model_file.save(path)
+ return path
+
+ @classmethod
+ def install_new_model(cls, yang_model_path: str):
+ logger.info("Installing new model: %s", yang_model_path)
+ command = "sysrepoctl --install --yang={} --owner=netconf:nogroup --permissions=777" \
+ .format(yang_model_path)
+ cls._run_bash_command(command)
+
+ @classmethod
+ def uninstall_a_model(cls, yang_model_name: str):
+ logger.info("Uninstalling a model: %s", yang_model_name)
+ command = "sysrepoctl --uninstall --module={}" \
+ .format(yang_model_name)
+ cls._run_bash_command(command)
+
+
+ @classmethod
+ def set_default_configuration(cls, init_conf_path: str, module_name: str):
+ logger.info("Attempting to set default configuration %s for module %s", init_conf_path, module_name)
+ command = "sysrepocfg --import={} --datastore=startup --format=xml --level=3 {}" \
+ .format(init_conf_path, module_name)
+ cls._run_bash_command(command)
+
+ @classmethod
+ def start_change_listener_for_model(cls, module_name: str):
+ logger.info("Starting listener for model: %s", module_name)
+ command = "python /netconf/netopeer_change_saver.py {} {} {}" \
+ .format(module_name, KAFKA_BROKER_NAME, KAFKA_TOPIC_NAME)
+ try:
+ check_output(["pgrep", "-f" , command], stderr=subprocess.STDOUT, universal_newlines=True)
+ logger.info("Change listener for {} already exist.".format(module_name))
+ except CalledProcessError:
+ subprocess.Popen(command.split(), stdout=subprocess.PIPE)
+
+ @classmethod
+ def stop_change_listener_for_model(cls, model_name):
+ logger.info("Stopping listener for model %s", model_name)
+ pid = cls.get_pid_by_name(model_name)
+ logger.info("pid is %s", pid)
+ command = "kill -2 {}".format(pid)
+ cls._run_bash_command(command)
+
+ @classmethod
+ def _run_bash_command(cls, command: str):
+ try:
+ logger.info("Attempts to invoke %s", command)
+ output = check_output(command.split(), stderr=subprocess.STDOUT,
+ universal_newlines=True)
+ logger.info("Output: %s", output)
+ if "ERR" in output:
+ raise RuntimeError(str(output))
+ except subprocess.CalledProcessError as e:
+ raise RuntimeError(e, str(e.stdout))
+
+ @classmethod
+ def get_pid_by_name(cls, name):
+ for dirname in os.listdir('/proc'):
+ if not dirname.isdigit():
+ continue
+ try:
+ with open('/proc/{}/cmdline'.format(dirname), mode='rb') as fd:
+ content = fd.read().decode().split('\x00')
+ except Exception as e:
+ print(e)
+ continue
+
+ if name in content:
+ return dirname
+
+
+class YangModelServer(Resource):
+ logger = logging.getLogger('YangModelServer')
+
+ def __init__(self, yang_loader_helper: YangLoaderHelper = YangLoaderHelper()):
+ self._yang_loader_helper = yang_loader_helper
+
+ def post(self):
+ args = self._parse_request()
+ yang_model_file = args['yangModel']
+ initial_config_file = args['initialConfig']
+ module_name = args['moduleName']
+ model_path = self._yang_loader_helper.save_file(yang_model_file)
+ conf_path = self._yang_loader_helper.save_file(initial_config_file)
+
+ try:
+ self._yang_loader_helper.install_new_model(model_path)
+ self._yang_loader_helper.set_default_configuration(conf_path,
+ module_name)
+ self._yang_loader_helper.start_change_listener_for_model(module_name)
+ except RuntimeError as e:
+ self.logger.error(e.args, exc_info=True)
+ return str(e.args), 400
+ return "Successfully started"
+
+ def delete(self):
+ args = self._parse_request()
+ yang_model_name = args['yangModelName']
+
+ try:
+ self._yang_loader_helper.stop_change_listener_for_model(yang_model_name)
+ time.sleep(5)
+ self._yang_loader_helper.uninstall_a_model(yang_model_name)
+ except RuntimeError as e:
+ self.logger.error(e.args, exc_info=True)
+ return str(e.args), 400
+ return "Successfully deleted"
+
+ @classmethod
+ def _parse_request(cls) -> reqparse.Namespace:
+ parse = reqparse.RequestParser()
+ parse.add_argument('yangModel',
+ type=FileStorage,
+ location='files')
+ parse.add_argument('initialConfig',
+ type=FileStorage,
+ location='files')
+ parse.add_argument('moduleName', type=str)
+ parse.add_argument('yangModelName', type=str)
+ return parse.parse_args()
+
+
+api.add_resource(YangModelServer, '/model')
+
+if __name__ == '__main__':
+ logging.basicConfig(filename=os.path.dirname(__file__) + "/yang_loader.log",
+ filemode="w",
+ level=logging.DEBUG)
+ app.run(host='0.0.0.0', port='5002')