summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorebo <eliezio.oliveira@est.tech>2020-04-11 01:34:47 +0100
committerBartek Grzybowski <b.grzybowski@partner.samsung.com>2020-04-15 10:46:07 +0000
commit398c9b251dc910c4cffc4fc5a3c2b8b221980c91 (patch)
treefa7df466e1a0f4a028445f58992686005e5c80f9
parent09e87eeadf879bbaa5237a34db1583861097925c (diff)
netconf-pnp-simulator: enable NETCONF send/recv message logging
to aid troubleshooting integration with OpenDaylight - Add more integration tests - Defaults to generic subscriber Issue-ID: INT-1516 Change-Id: Ib5bbf4cdbba6cdfee901f6c07dfa195a21cd8bbb Signed-off-by: ebo <eliezio.oliveira@est.tech>
-rw-r--r--test/mocks/netconf-pnp-simulator/docs/README.rst9
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/Dockerfile4
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/common.sh21
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/config/modules/.gitkeep0
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/startup.xml72
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/turing-machine.yang262
-rwxr-xr-xtest/mocks/netconf-pnp-simulator/engine/configure-modules.sh11
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/container-tag.yaml2
-rwxr-xr-xtest/mocks/netconf-pnp-simulator/engine/generic_subscriber.py133
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/patches/libnetconf2/04-io-log.patch27
-rwxr-xr-xtest/mocks/netconf-pnp-simulator/engine/reconfigure-ssh.sh9
-rwxr-xr-xtest/mocks/netconf-pnp-simulator/engine/reconfigure-tls.sh11
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/templates/ietf-keystore.xml (renamed from test/mocks/netconf-pnp-simulator/engine/templates/load_server_certs.xml)0
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/templates/ietf-netconf-server.xml (renamed from test/mocks/netconf-pnp-simulator/engine/templates/tls_listen.xml)0
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/templates/ietf-system.xml (renamed from test/mocks/netconf-pnp-simulator/engine/templates/load_auth_pubkey.xml)0
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/tests/nctest.py55
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/tests/settings.py2
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py37
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/tests/test_turing_machine.py130
-rw-r--r--test/mocks/netconf-pnp-simulator/engine/tox.ini9
20 files changed, 717 insertions, 77 deletions
diff --git a/test/mocks/netconf-pnp-simulator/docs/README.rst b/test/mocks/netconf-pnp-simulator/docs/README.rst
index 452827970..ec2a15834 100644
--- a/test/mocks/netconf-pnp-simulator/docs/README.rst
+++ b/test/mocks/netconf-pnp-simulator/docs/README.rst
@@ -3,9 +3,6 @@ NETCONF Plug-and-Play Simulator
.. sectnum::
-.. _py-requirements: https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
-.. _yang-rfc: https://tools.ietf.org/html/rfc6020
-
|ci-badge| |release-badge| |docker-badge|
.. |ci-badge| image:: https://github.com/blue-onap/netconf-pnp-simulator/workflows/CI/badge.svg
@@ -42,13 +39,13 @@ A YANG module contains the following files:
* - Filename
- Purpose
* - ``model.yang``
- - The YANG model specified according to `RFC-6020 <yang-rfc_>`_ and named after the module's name, e.g., *mynetconf.yang*.
+ - The YANG model specified according to `RFC-6020 <https://tools.ietf.org/html/rfc6020>`_ and named after the module's name, e.g., *mynetconf.yang*.
* - ``startup.json`` or ``startup.xml``
- An optional data file with the initial values of the model. Both JSON and XML formats are supported.
* - ``subscriber.py``
- - The Python 3 application that implements the behavioral aspects of the YANG model.
+ - The Python 3 application that implements the behavioral aspects of the YANG model. If you don't supply one, a generic subscriber that logs all received events will be used.
* - ``requirements.txt``
- - [Optional] Lists the additional Python packages required by the application, specified in the `Requirements File Format <py-requirements_>`_.
+ - [Optional] Lists the additional Python packages required by the application, specified in the `Requirements File Format <https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format>`_.
Application
-----------
diff --git a/test/mocks/netconf-pnp-simulator/engine/Dockerfile b/test/mocks/netconf-pnp-simulator/engine/Dockerfile
index a3f8b6ac4..9eec0baa7 100644
--- a/test/mocks/netconf-pnp-simulator/engine/Dockerfile
+++ b/test/mocks/netconf-pnp-simulator/engine/Dockerfile
@@ -189,9 +189,9 @@ RUN mkdir /etc/supervisord.d
COPY zlog.conf /opt/etc/
# Sensible defaults for loguru configuration
-ENV LOGURU_FORMAT="<green>{time:YYYY-DD-MM HH:mm:ss.SSS}</green> {level: <5} [mynetconf] <lvl>{message}</lvl>"
+ENV LOGURU_FORMAT="<green>{time:YYYY-DD-MM HH:mm:ss.SSS}</green> {level: <5} [{module}] <lvl>{message}</lvl>"
ENV LOGURU_COLORIZE=True
-COPY entrypoint.sh common.sh configure-*.sh reconfigure-*.sh /opt/bin/
+COPY entrypoint.sh common.sh configure-*.sh reconfigure-*.sh generic_subscriber.py /opt/bin/
CMD /opt/bin/entrypoint.sh
diff --git a/test/mocks/netconf-pnp-simulator/engine/common.sh b/test/mocks/netconf-pnp-simulator/engine/common.sh
index 6e938e7f5..961d51f9b 100644
--- a/test/mocks/netconf-pnp-simulator/engine/common.sh
+++ b/test/mocks/netconf-pnp-simulator/engine/common.sh
@@ -32,6 +32,9 @@ TEMPLATES=/templates
PROC_NAME=${0##*/}
PROC_NAME=${PROC_NAME%.sh}
+WORKDIR=$(mktemp -d)
+trap "rm -rf $WORKDIR" EXIT
+
function now_ms() {
# Requires coreutils package
date +"%Y-%m-%d %H:%M:%S.%3N"
@@ -57,10 +60,16 @@ find_file() {
# Extracts the body of a PEM file by removing the dashed header and footer
-pem_body() {
- grep -Fv -- ----- "$1"
-}
+alias pem_body='grep -Fv -- -----'
+
+kill_service() {
+ local service=$1
+
+ pid=$(cat /var/run/${service}.pid)
+ log INFO Killing $service pid=$pid
+ kill $pid
+}
# ------------------------------------
# SSH Common Definitions and Functions
@@ -83,7 +92,7 @@ configure_ssh() {
--update '//_:name[text()="netconf"]/following-sibling::_:authorized-key/_:name' --value "$name" \
--update '//_:name[text()="netconf"]/following-sibling::_:authorized-key/_:algorithm' --value "$1" \
--update '//_:name[text()="netconf"]/following-sibling::_:authorized-key/_:key-data' --value "$2" \
- $dir/load_auth_pubkey.xml | \
+ $dir/ietf-system.xml | \
sysrepocfg --datastore=$datastore --permanent --format=xml ietf-system --${operation}=-
}
@@ -109,13 +118,13 @@ configure_tls() {
xmlstarlet ed --pf --omit-decl \
--update '//_:name[text()="server_cert"]/following-sibling::_:certificate' --value "$server_cert" \
--update '//_:name[text()="ca"]/following-sibling::_:certificate' --value "$ca_cert" \
- $dir/load_server_certs.xml | \
+ $dir/ietf-keystore.xml | \
sysrepocfg --datastore=$datastore --permanent --format=xml ietf-keystore --${operation}=-
log INFO Configure TLS ingress service
ca_fingerprint=$(openssl x509 -noout -fingerprint -in $TLS_CONFIG/ca.pem | cut -d= -f2)
xmlstarlet ed --pf --omit-decl \
--update '//_:name[text()="netconf"]/preceding-sibling::_:fingerprint' --value "02:$ca_fingerprint" \
- $dir/tls_listen.xml | \
+ $dir/ietf-netconf-server.xml | \
sysrepocfg --datastore=$datastore --permanent --format=xml ietf-netconf-server --${operation}=-
}
diff --git a/test/mocks/netconf-pnp-simulator/engine/config/modules/.gitkeep b/test/mocks/netconf-pnp-simulator/engine/config/modules/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
--- a/test/mocks/netconf-pnp-simulator/engine/config/modules/.gitkeep
+++ /dev/null
diff --git a/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/startup.xml b/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/startup.xml
new file mode 100644
index 000000000..453b3accf
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/startup.xml
@@ -0,0 +1,72 @@
+<turing-machine xmlns="http://example.net/turing-machine">
+ <transition-function>
+ <delta>
+ <label>left summand</label>
+ <input>
+ <state>0</state>
+ <symbol>1</symbol>
+ </input>
+ </delta>
+ <delta>
+ <label>separator</label>
+ <input>
+ <state>0</state>
+ <symbol>0</symbol>
+ </input>
+ <output>
+ <state>1</state>
+ <symbol>1</symbol>
+ </output>
+ </delta>
+ <delta>
+ <label>right summand</label>
+ <input>
+ <state>1</state>
+ <symbol>1</symbol>
+ </input>
+ </delta>
+ <delta>
+ <label>right end</label>
+ <input>
+ <state>1</state>
+ <symbol/>
+ </input>
+ <output>
+ <state>2</state>
+ <head-move>left</head-move>
+ </output>
+ </delta>
+ <delta>
+ <label>write separator</label>
+ <input>
+ <state>2</state>
+ <symbol>1</symbol>
+ </input>
+ <output>
+ <state>3</state>
+ <symbol>0</symbol>
+ <head-move>left</head-move>
+ </output>
+ </delta>
+ <delta>
+ <label>go home</label>
+ <input>
+ <state>3</state>
+ <symbol>1</symbol>
+ </input>
+ <output>
+ <head-move>left</head-move>
+ </output>
+ </delta>
+ <delta>
+ <label>final step</label>
+ <input>
+ <state>3</state>
+ <symbol/>
+ </input>
+ <output>
+ <state>4</state>
+ </output>
+ </delta>
+ </transition-function>
+</turing-machine>
diff --git a/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/turing-machine.yang b/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/turing-machine.yang
new file mode 100644
index 000000000..abd6794b0
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/config/modules/turing-machine/turing-machine.yang
@@ -0,0 +1,262 @@
+module turing-machine {
+
+ namespace "http://example.net/turing-machine";
+
+ prefix "tm";
+
+ description
+ "Data model for the Turing Machine.";
+
+ revision 2013-12-27 {
+ description
+ "Initial revision.";
+ }
+
+ /* Typedefs */
+
+ typedef tape-symbol {
+ type string {
+ length "0..1";
+ }
+ description
+ "Type of symbols appearing in tape cells.
+
+ A blank is represented as an empty string where necessary.";
+ }
+
+ typedef cell-index {
+ type int64;
+ description
+ "Type for indexing tape cells.";
+ }
+
+ typedef state-index {
+ type uint16;
+ description
+ "Type for indexing states of the control unit.";
+ }
+
+ typedef head-dir {
+ type enumeration {
+ enum left;
+ enum right;
+ }
+ default "right";
+ description
+ "Possible directions for moving the read/write head, one cell
+ to the left or right (default).";
+ }
+
+ /* Groupings */
+
+ grouping tape-cells {
+ description
+ "The tape of the Turing Machine is represented as a sparse
+ array.";
+ list cell {
+ key "coord";
+ description
+ "List of non-blank cells.";
+ leaf coord {
+ type cell-index;
+ description
+ "Coordinate (index) of the tape cell.";
+ }
+ leaf symbol {
+ type tape-symbol {
+ length "1";
+ }
+ description
+ "Symbol appearing in the tape cell.
+
+ Blank (empty string) is not allowed here because the
+ 'cell' list only contains non-blank cells.";
+ }
+ }
+ }
+
+ /* State data and Configuration */
+
+ container turing-machine {
+ description
+ "State data and configuration of a Turing Machine.";
+ leaf state {
+ type state-index;
+ config "false";
+ mandatory "true";
+ description
+ "Current state of the control unit.
+
+ The initial state is 0.";
+ }
+ leaf head-position {
+ type cell-index;
+ config "false";
+ mandatory "true";
+ description
+ "Position of tape read/write head.";
+ }
+ container tape {
+ config "false";
+ description
+ "The contents of the tape.";
+ uses tape-cells;
+ }
+ container transition-function {
+ description
+ "The Turing Machine is configured by specifying the
+ transition function.";
+ list delta {
+ key "label";
+ unique "input/state input/symbol";
+ description
+ "The list of transition rules.";
+ leaf label {
+ type string;
+ description
+ "An arbitrary label of the transition rule.";
+ }
+ container input {
+ description
+ "Input parameters (arguments) of the transition rule.";
+ leaf state {
+ type state-index;
+ mandatory "true";
+ description
+ "Current state of the control unit.";
+ }
+ leaf symbol {
+ type tape-symbol;
+ mandatory "true";
+ description
+ "Symbol read from the tape cell.";
+ }
+ }
+ container output {
+ description
+ "Output values of the transition rule.";
+ leaf state {
+ type state-index;
+ description
+ "New state of the control unit. If this leaf is not
+ present, the state doesn't change.";
+ }
+ leaf symbol {
+ type tape-symbol;
+ description
+ "Symbol to be written to the tape cell. If this leaf is
+ not present, the symbol doesn't change.";
+ }
+ leaf head-move {
+ type head-dir;
+ description
+ "Move the head one cell to the left or right";
+ }
+ }
+ }
+ }
+ }
+
+ /* RPCs */
+
+ rpc initialize {
+ description
+ "Initialize the Turing Machine as follows:
+
+ 1. Put the control unit into the initial state (0).
+
+ 2. Move the read/write head to the tape cell with coordinate
+ zero.
+
+ 3. Write the string from the 'tape-content' input parameter to
+ the tape, character by character, starting at cell 0. The
+ tape is othewise empty.";
+ input {
+ leaf tape-content {
+ type string;
+ default "";
+ description
+ "The string with which the tape shall be initialized. The
+ leftmost symbol will be at tape coordinate 0.";
+ }
+ }
+ }
+
+ rpc run {
+ description
+ "Start the Turing Machine operation.";
+ }
+
+ rpc run-until {
+ description
+ "Start the Turing Machine operation and let it run until it is halted
+ or ALL the defined breakpoint conditions are satisfied.";
+ input {
+ leaf state {
+ type state-index;
+ description
+ "What state the control unit has to be at for the execution to be paused.";
+ }
+ leaf head-position {
+ type cell-index;
+ description
+ "Position of tape read/write head for which the breakpoint applies.";
+ }
+ container tape {
+ description
+ "What content the tape has to have for the breakpoint to apply.";
+ uses tape-cells;
+ }
+ }
+ output {
+ leaf step-count {
+ type uint64;
+ description
+ "The number of steps executed since the last 'run-until' call.";
+ }
+ leaf halted {
+ type boolean;
+ description
+ "'True' if the Turing machine is halted, 'false' if it is only paused.";
+ }
+ }
+ }
+
+ /* Notifications */
+
+ notification halted {
+ description
+ "The Turing Machine has halted. This means that there is no
+ transition rule for the current state and tape symbol.";
+ leaf state {
+ type state-index;
+ mandatory "true";
+ description
+ "The state of the control unit in which the machine has
+ halted.";
+ }
+ }
+
+ notification paused {
+ description
+ "The Turing machine has reached a breakpoint and was paused.";
+ leaf state {
+ type state-index;
+ mandatory "true";
+ description
+ "State of the control unit in which the machine was paused.";
+ }
+ leaf head-position {
+ type cell-index;
+ mandatory "true";
+ description
+ "Position of tape read/write head when the machine was paused.";
+ }
+ container tape {
+ description
+ "Content of the tape when the machine was paused.";
+ uses tape-cells;
+ }
+ }
+}
+
diff --git a/test/mocks/netconf-pnp-simulator/engine/configure-modules.sh b/test/mocks/netconf-pnp-simulator/engine/configure-modules.sh
index 2010b504f..d40918f31 100755
--- a/test/mocks/netconf-pnp-simulator/engine/configure-modules.sh
+++ b/test/mocks/netconf-pnp-simulator/engine/configure-modules.sh
@@ -26,6 +26,7 @@ source $HERE/common.sh
MODELS_CONFIG=$CONFIG/modules
BASE_VIRTUALENVS=$HOME/.local/share/virtualenvs
+GENERIC_SUBSCRIBER=/opt/bin/generic_subscriber.py
install_and_configure_yang_model()
{
@@ -54,6 +55,8 @@ configure_subscriber_execution()
APP_PATH=$env_dir/bin:$APP_PATH
fi
log INFO Preparing launching of module \"$model\" application
+ # shellcheck disable=SC2153
+ loguru_format="${LOGURU_FORMAT//\{module\}/$model}"
cat > /etc/supervisord.d/$model.conf <<EOF
[program:subs-$model]
command=$app $model
@@ -61,7 +64,7 @@ stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
redirect_stderr=true
autorestart=true
-environment=PATH=$APP_PATH,PYTHONUNBUFFERED="1"
+environment=PATH=$APP_PATH,PYTHONUNBUFFERED="1",LOGURU_FORMAT="$loguru_format"
EOF
}
@@ -89,7 +92,11 @@ for dir in "$MODELS_CONFIG"/*; do
install_and_configure_yang_model $dir $model
app="$dir/subscriber.py"
if [ -x "$app" ]; then
- configure_subscriber_execution $dir $model $app
+ log INFO Module $model is using its own subscriber
+ else
+ log WARN Module $model is using the generic subscriber
+ app=$GENERIC_SUBSCRIBER
fi
+ configure_subscriber_execution $dir $model $app
fi
done
diff --git a/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml b/test/mocks/netconf-pnp-simulator/engine/container-tag.yaml
index 75e0ac656..9bd214eca 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.8.2"
+tag: "2.8.4"
diff --git a/test/mocks/netconf-pnp-simulator/engine/generic_subscriber.py b/test/mocks/netconf-pnp-simulator/engine/generic_subscriber.py
new file mode 100755
index 000000000..66fd7b6ab
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/generic_subscriber.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+
+__author__ = "Mislav Novakovic <mislav.novakovic@sartura.hr>"
+__copyright__ = "Copyright 2018, Deutsche Telekom AG"
+__license__ = "Apache 2.0"
+
+# 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.
+
+# This sample application demonstrates use of Python programming language bindings for sysrepo library.
+# Original c application was rewritten in Python to show similarities and differences
+# between the two.
+#
+# Most notable difference is in the very different nature of languages, c is weakly statically typed language
+# while Python is strongly dynamically typed. Python code is much easier to read and logic easier to comprehend
+# for smaller scripts. Memory safety is not an issue but lower performance can be expected.
+#
+# The original c implementation is also available in the source, so one can refer to it to evaluate trade-offs.
+
+import sys
+
+import sysrepo as sr
+from loguru import logger
+
+
+# Helper function for printing changes given operation, old and new value.
+def print_change(op, old_val, new_val):
+ if op == sr.SR_OP_CREATED:
+ logger.info(f"CREATED: {new_val.to_string()}")
+ elif op == sr.SR_OP_DELETED:
+ logger.info(f"DELETED: {old_val.to_string()}")
+ elif op == sr.SR_OP_MODIFIED:
+ logger.info(f"MODIFIED: {old_val.to_string()} to {new_val.to_string()}")
+ elif op == sr.SR_OP_MOVED:
+ logger.info(f"MOVED: {new_val.xpath()} after {old_val.xpath()}")
+
+
+# Helper function for printing events.
+def ev_to_str(ev):
+ if ev == sr.SR_EV_VERIFY:
+ return "verify"
+ elif ev == sr.SR_EV_APPLY:
+ return "apply"
+ elif ev == sr.SR_EV_ABORT:
+ return "abort"
+ else:
+ return "unknown"
+
+
+# Function to print current configuration state.
+# It does so by loading all the items of a session and printing them out.
+def print_current_config(session, module_name):
+ select_xpath = f"/{module_name}:*//*"
+
+ values = session.get_items(select_xpath)
+
+ if values is not None:
+ logger.info("========== BEGIN CONFIG ==========")
+ for i in range(values.val_cnt()):
+ logger.info(f" {values.val(i).to_string().strip()}")
+ logger.info("=========== END CONFIG ===========")
+
+
+# Function to be called for subscribed client of given session whenever configuration changes.
+def module_change_cb(sess, module_name, event, private_ctx):
+ try:
+ logger.info("========== Notification " + ev_to_str(event) + " =============================================")
+ if event == sr.SR_EV_APPLY:
+ print_current_config(sess, module_name)
+
+ logger.info("========== CHANGES: =============================================")
+
+ change_path = f"/{module_name}:*"
+
+ it = sess.get_changes_iter(change_path)
+
+ while True:
+ change = sess.get_change_next(it)
+ if change is None:
+ break
+ print_change(change.oper(), change.old_val(), change.new_val())
+
+ logger.info("========== END OF CHANGES =======================================")
+ except Exception as e:
+ logger.error(e)
+
+ return sr.SR_ERR_OK
+
+
+def main():
+ # Notable difference between c implementation is using exception mechanism for open handling unexpected events.
+ # Here it is useful because `Connection`, `Session` and `Subscribe` could throw an exception.
+ try:
+ module_name = sys.argv[1]
+ logger.info(f"Application will watch for changes in {module_name}")
+
+ # connect to sysrepo
+ conn = sr.Connection(module_name)
+
+ # start session
+ sess = sr.Session(conn)
+
+ # subscribe for changes in running config */
+ subscribe = sr.Subscribe(sess)
+
+ subscribe.module_change_subscribe(module_name, module_change_cb)
+
+ try:
+ print_current_config(sess, module_name)
+ except Exception as e:
+ logger.error(e)
+
+ logger.info("========== STARTUP CONFIG APPLIED AS RUNNING ==========")
+
+ sr.global_loop()
+
+ logger.info("Application exit requested, exiting.")
+
+ except Exception as e:
+ logger.error(e)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/mocks/netconf-pnp-simulator/engine/patches/libnetconf2/04-io-log.patch b/test/mocks/netconf-pnp-simulator/engine/patches/libnetconf2/04-io-log.patch
new file mode 100644
index 000000000..8c83e4b15
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/patches/libnetconf2/04-io-log.patch
@@ -0,0 +1,27 @@
+diff --git a/src/io.c b/src/io.c
+index 9c4fa9f..830fc9a 100644
+--- a/src/io.c
++++ b/src/io.c
+@@ -432,7 +432,7 @@ nc_read_msg_io(struct nc_session *session, int io_timeout, struct lyxml_elem **d
+ nc_session_io_unlock(session, __func__);
+ io_locked = 0;
+
+- DBG("Session %u: received message:\n%s\n", session->id, msg);
++ VRB("Session %u: received message:\n%s", session->id, msg);
+
+ /* build XML tree */
+ *data = lyxml_parse_mem(session->ctx, msg, 0);
+@@ -718,7 +718,7 @@ nc_write(struct nc_session *session, const void *buf, size_t count)
+ return -1;
+ }
+
+- DBG("Session %u: sending message:\n%.*s\n", session->id, count, buf);
++ VRB("Session %u: sending message:\n%.*s", session->id, count, buf);
+
+ do {
+ switch (session->ti_type) {
+@@ -1346,4 +1346,3 @@ nc_realloc(void *ptr, size_t size)
+
+ return ret;
+ }
+-
diff --git a/test/mocks/netconf-pnp-simulator/engine/reconfigure-ssh.sh b/test/mocks/netconf-pnp-simulator/engine/reconfigure-ssh.sh
index 2634dc116..7d3863340 100755
--- a/test/mocks/netconf-pnp-simulator/engine/reconfigure-ssh.sh
+++ b/test/mocks/netconf-pnp-simulator/engine/reconfigure-ssh.sh
@@ -26,12 +26,7 @@ source $HERE/common.sh
SSH_CONFIG=$CONFIG/ssh
-WORKDIR=$(mktemp -d)
-trap "rm -rf $WORKDIR" EXIT
-
-sysrepocfg --format=xml --export=$WORKDIR/load_auth_pubkey.xml ietf-system
+sysrepocfg --format=xml --export=$WORKDIR/ietf-system.xml ietf-system
configure_ssh running import $WORKDIR
-pid=$(cat /var/run/netopeer2-server.pid)
-log INFO Restart Netopeer2 pid=$pid
-kill $pid
+kill_service netopeer2-server
diff --git a/test/mocks/netconf-pnp-simulator/engine/reconfigure-tls.sh b/test/mocks/netconf-pnp-simulator/engine/reconfigure-tls.sh
index 6c97064ee..10f32873a 100755
--- a/test/mocks/netconf-pnp-simulator/engine/reconfigure-tls.sh
+++ b/test/mocks/netconf-pnp-simulator/engine/reconfigure-tls.sh
@@ -24,13 +24,8 @@ set -eu
HERE=${0%/*}
source $HERE/common.sh
-WORKDIR=$(mktemp -d)
-trap "rm -rf $WORKDIR" EXIT
-
-sysrepocfg --format=xml --export=$WORKDIR/load_server_certs.xml ietf-keystore
-sysrepocfg --format=xml --export=$WORKDIR/tls_listen.xml ietf-netconf-server
+sysrepocfg --format=xml --export=$WORKDIR/ietf-keystore.xml ietf-keystore
+sysrepocfg --format=xml --export=$WORKDIR/ietf-netconf-server.xml ietf-netconf-server
configure_tls running import $WORKDIR
-pid=$(cat /var/run/netopeer2-server.pid)
-log INFO Restart Netopeer2 pid=$pid
-kill $pid
+kill_service netopeer2-server
diff --git a/test/mocks/netconf-pnp-simulator/engine/templates/load_server_certs.xml b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-keystore.xml
index ef02dedef..ef02dedef 100644
--- a/test/mocks/netconf-pnp-simulator/engine/templates/load_server_certs.xml
+++ b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-keystore.xml
diff --git a/test/mocks/netconf-pnp-simulator/engine/templates/tls_listen.xml b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-netconf-server.xml
index a6b6bedb1..a6b6bedb1 100644
--- a/test/mocks/netconf-pnp-simulator/engine/templates/tls_listen.xml
+++ b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-netconf-server.xml
diff --git a/test/mocks/netconf-pnp-simulator/engine/templates/load_auth_pubkey.xml b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-system.xml
index 93b662f02..93b662f02 100644
--- a/test/mocks/netconf-pnp-simulator/engine/templates/load_auth_pubkey.xml
+++ b/test/mocks/netconf-pnp-simulator/engine/templates/ietf-system.xml
diff --git a/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py b/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py
index 2f848c361..11ff6ffc4 100644
--- a/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/nctest.py
@@ -1,11 +1,41 @@
+import logging.config
+
from ncclient import manager, operations
+
import settings
-import unittest
-class NCTestCase(unittest.TestCase):
+LOGGER = logging.getLogger(__name__)
+
+
+def check_reply_ok(reply):
+ assert reply is not None
+ _log_netconf_msg("Received", reply.xml)
+ assert reply.ok is True
+ assert reply.error is None
+
+
+def check_reply_err(reply):
+ assert reply is not None
+ _log_netconf_msg("Received", reply.xml)
+ assert reply.ok is False
+ assert reply.error is not None
+
+
+def check_reply_data(reply):
+ check_reply_ok(reply)
+
+
+def _log_netconf_msg(header: str, body: str):
+ """Log a message using a format inspired by NETCONF 1.1 """
+ LOGGER.info("%s:\n\n#%d\n%s\n##", header, len(body), body)
+
+
+class NCTestCase:
""" Base class for NETCONF test cases. Provides a NETCONF connection and some helper methods. """
- def setUp(self):
+ nc: manager.Manager
+
+ def setup(self):
self.nc = manager.connect(
host=settings.HOST,
port=settings.PORT,
@@ -16,22 +46,5 @@ class NCTestCase(unittest.TestCase):
hostkey_verify=False)
self.nc.raise_mode = operations.RaiseMode.NONE
- def tearDown(self):
+ 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
index 716fdb7a2..124e333cd 100644
--- a/test/mocks/netconf-pnp-simulator/engine/tests/settings.py
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/settings.py
@@ -5,5 +5,3 @@ HOST = "127.0.0.1"
PORT = int(os.environ["NETCONF_PNP_SIMULATOR_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
index 62d41c259..06164e6b5 100644
--- a/test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/test_basic_operations.py
@@ -1,52 +1,49 @@
-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)
+ assert ":startup" in self.nc.server_capabilities
+ assert ":candidate" in self.nc.server_capabilities
+ assert ":validate" in self.nc.server_capabilities
+ assert ":xpath" in self.nc.server_capabilities
def test_get(self):
reply = self.nc.get()
- self.check_reply_data(reply)
+ nctest.check_reply_data(reply)
def test_get_config_startup(self):
reply = self.nc.get_config(source='startup')
- self.check_reply_data(reply)
+ nctest.check_reply_data(reply)
def test_get_config_running(self):
reply = self.nc.get_config(source='running')
- self.check_reply_data(reply)
+ nctest.check_reply_data(reply)
def test_copy_config(self):
reply = self.nc.copy_config(source='startup', target='candidate')
- self.check_reply_ok(reply)
+ nctest.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)
+ nctest.check_reply_err(reply)
def test_lock(self):
reply = self.nc.lock("startup")
- self.check_reply_ok(reply)
+ nctest.check_reply_ok(reply)
reply = self.nc.lock("running")
- self.check_reply_ok(reply)
+ nctest.check_reply_ok(reply)
reply = self.nc.lock("candidate")
- self.check_reply_ok(reply)
+ nctest.check_reply_ok(reply)
reply = self.nc.lock("startup")
- self.check_reply_err(reply)
+ nctest.check_reply_err(reply)
reply = self.nc.unlock("startup")
- self.check_reply_ok(reply)
+ nctest.check_reply_ok(reply)
reply = self.nc.unlock("running")
- self.check_reply_ok(reply)
+ nctest.check_reply_ok(reply)
reply = self.nc.unlock("candidate")
- self.check_reply_ok(reply)
-
-if __name__ == '__main__':
- unittest.main()
+ nctest.check_reply_ok(reply)
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 000000000..8ac38b0f5
--- /dev/null
+++ b/test/mocks/netconf-pnp-simulator/engine/tests/test_turing_machine.py
@@ -0,0 +1,130 @@
+import nctest
+
+_NAMESPACES = {
+ "nc": "urn:ietf:params:xml:ns:netconf:base:1.0",
+ "tm": "http://example.net/turing-machine"
+}
+
+
+def check_labels_only_in_data(data):
+ children = data.xpath("/nc:rpc-reply/nc:data/*", namespaces=_NAMESPACES)
+ assert children
+ for child in children:
+ assert child.tag.endswith("turing-machine")
+ children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/*", namespaces=_NAMESPACES)
+ assert children
+ for child in children:
+ assert child.tag.endswith("transition-function")
+ children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/*", namespaces=_NAMESPACES)
+ assert children
+ for child in children:
+ assert child.tag.endswith("delta")
+ children = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/tm:delta/*",
+ namespaces=_NAMESPACES)
+ assert children
+ for child in children:
+ assert child.tag.endswith("label")
+
+
+def check_deltas_in_data(data):
+ deltas = data.xpath("/nc:rpc-reply/nc:data/tm:turing-machine/tm:transition-function/*", namespaces=_NAMESPACES)
+ assert deltas
+ for d in deltas:
+ assert d.tag.endswith("delta")
+
+
+class TestTuringMachine(nctest.NCTestCase):
+ """ Tests basic NETCONF operations on the turing-machine YANG module. """
+
+ def test_get(self):
+ reply = self.nc.get()
+ nctest.check_reply_data(reply)
+ check_deltas_in_data(reply.data)
+
+ def test_get_config_startup(self):
+ reply = self.nc.get_config(source="startup")
+ nctest.check_reply_data(reply)
+ check_deltas_in_data(reply.data)
+
+ def test_get_config_running(self):
+ reply = self.nc.get_config(source="running")
+ nctest.check_reply_data(reply)
+ 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)
+ nctest.check_reply_data(reply)
+ check_deltas_in_data(reply.data)
+ 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)
+ nctest.check_reply_data(reply)
+ check_deltas_in_data(reply.data)
+ check_labels_only_in_data(reply.data)
+
+ 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))
+ nctest.check_reply_ok(reply)
+ # get
+ reply = self.nc.get_config(source="running")
+ nctest.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=_NAMESPACES)
+ assert len(deltas) == 1
+ # create already existing - expect error
+ reply = self.nc.edit_config(target='running', config=config_xml.format("create", 9, 99))
+ nctest.check_reply_err(reply)
+ # replace
+ reply = self.nc.edit_config(target='running', config=config_xml.format("replace", 9, 88))
+ nctest.check_reply_ok(reply)
+ # get
+ reply = self.nc.get_config(source="running")
+ nctest.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=_NAMESPACES)
+ assert len(states) == 1
+ assert states[0].text == "88"
+ # delete
+ reply = self.nc.edit_config(target='running', config=config_xml.format("delete", 9, 88))
+ nctest.check_reply_ok(reply)
+ # delete non-existing - expect error
+ reply = self.nc.edit_config(target='running', config=config_xml.format("delete", 9, 88))
+ nctest.check_reply_err(reply)
+ # get - should be empty
+ reply = self.nc.get_config(source="running")
+ nctest.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=_NAMESPACES)
+ assert not deltas
diff --git a/test/mocks/netconf-pnp-simulator/engine/tox.ini b/test/mocks/netconf-pnp-simulator/engine/tox.ini
index 4b0ac1efe..20870cf5e 100644
--- a/test/mocks/netconf-pnp-simulator/engine/tox.ini
+++ b/test/mocks/netconf-pnp-simulator/engine/tox.ini
@@ -18,6 +18,7 @@
# ============LICENSE_END=========================================================
[tox]
+envlist = py3
requires = tox-docker
skipsdist = True
@@ -27,6 +28,10 @@ docker =
netconf-pnp-simulator:latest
deps =
+ pytest
ncclient
- discover
-commands = discover -v
+commands = pytest -v
+
+[pytest]
+log_level = INFO
+log_format = %(asctime)s.%(msecs)03d %(levelname)-5s [%(name)s] %(message)s