Skip to main content

A pytest plugin that provides a mock NETCONF (RFC6241/RFC6242) server for local testing.

Project description

pytest-netconf

GitHub Actions Workflow Status codecov PyPI - Python Version PyPI - Downloads GitHub License

A pytest plugin that provides a mock NETCONF (RFC6241/RFC6242) server for local testing.

pytest-netconf is authored by Adam Kirchberger, governed as a benevolent dictatorship, and distributed under license.

Introduction

Testing NETCONF devices has traditionally required maintaining labs with multiple vendor devices which can be complex and resource-intensive. Additionally, spinning up virtual devices for testing purposes is often time-consuming and too slow for CICD pipelines. This plugin provides a convenient way to mock the behavior and responses of these NETCONF devices.

Features

  • NETCONF server, a real SSH server is run locally which enables testing using actual network connections instead of patching.
  • Predefined requests and responses, define specific NETCONF requests and responses to meet your testing needs.
  • Capability testing, define specific capabilities you want the server to support and test their responses.
  • Authentication testing, test error handling for authentication issues (supports password or key auth).
  • Connection testing, test error handling when tearing down connections unexpectedly.

NETCONF Clients

The clients below have been tested

  • ncclient :white_check_mark:
  • netconf-client :white_check_mark:
  • scrapli-netconf :white_check_mark:

Installation

Install using pip install pytest-netconf or poetry add --group dev pytest-netconf

Quickstart

The plugin will install a pytest fixture named netconf_server, which will start an SSH server with settings you provide, and only reply to requests which you define with corresponding responses.

For more use cases see examples

# Configure server settings
netconf_server.username = None  # allow any username
netconf_server.password = None  # allow any password
netconf_server.port = 8830  # default value

# Configure a request and response
netconf_server.expect_request(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<nc:rpc xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="{message_id}">'
        "<nc:get-config><nc:source><nc:running/></nc:source></nc:get-config>"
        "</nc:rpc>"
    ).respond_with(
        """
        <?xml version="1.0" encoding="UTF-8"?>
        <rpc-reply message-id="{message_id}"
          xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
            <data>
                <interfaces>
                    <interface>
                        <name>eth0</name>
                    </interface>
                </interfaces>
            </data>
        </rpc-reply>
        """
    )

Examples

Get Config
from pytest_netconf import NetconfServer
from ncclient import manager


def test_netconf_get_config(
    netconf_server: NetconfServer,
):
    # GIVEN server request and response
    netconf_server.expect_request(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<nc:rpc xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="{message_id}">'
        "<nc:get-config><nc:source><nc:running/></nc:source></nc:get-config>"
        "</nc:rpc>"
    ).respond_with(
        """
        <?xml version="1.0" encoding="UTF-8"?>
        <rpc-reply message-id="{message_id}"
          xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
            <data>
                <interfaces>
                    <interface>
                        <name>eth0</name>
                    </interface>
                </interfaces>
            </data>
        </rpc-reply>"""
    )

    # WHEN fetching rpc response from server
    with manager.connect(
        host="localhost",
        port=8830,
        username="admin",
        password="admin",
        hostkey_verify=False,
    ) as m:
        response = m.get_config(source="running").data_xml

    # THEN expect response
    assert (
        """
                <interfaces>
                    <interface>
                        <name>eth0</name>
                    </interface>
                </interfaces>
        """
        in response
    )
Authentication Fail
from pytest_netconf import NetconfServer
from ncclient import manager
from ncclient.transport.errors import AuthenticationError


def test_netconf_auth_fail(
    netconf_server: NetconfServer,
):
    # GIVEN username and password have been defined
    netconf_server.username = "admin"
    netconf_server.password = "password"

    # WHEN connecting using wrong credentials
    with pytest.raises(AuthenticationError) as error:
        with manager.connect(
            host="localhost",
            port=8830,
            username="foo",
            password="bar",
            hostkey_verify=False,
        ):
            ...

    # THEN expect error
    assert error
Custom Capabilities
from pytest_netconf import NetconfServer
from ncclient import manager


def test_netconf_capabilities(
    netconf_server: NetconfServer,
):
    # GIVEN extra capabilities
    netconf_server.capabilities.append("urn:ietf:params:netconf:capability:foo:1.1")
    netconf_server.capabilities.append("urn:ietf:params:netconf:capability:bar:1.1")

    # WHEN receiving server capabilities
    with manager.connect(
        host="localhost",
        port=8830,
        username="admin",
        password="admin",
        hostkey_verify=False,
    ) as m:
        server_capabilities = m.server_capabilities

    # THEN expect to see capabilities
    assert "urn:ietf:params:netconf:capability:foo:1.1" in server_capabilities
    assert "urn:ietf:params:netconf:capability:bar:1.1" in server_capabilities
Server Disconnect
from pytest_netconf import NetconfServer
from ncclient import manager
from ncclient.transport.errors import TransportError


def test_netconf_server_disconnect(
    netconf_server: NetconfServer,
):
    # GIVEN netconf connection
    with pytest.raises(TransportError) as error:
        with manager.connect(
            host="localhost",
            port=8830,
            username="admin",
            password="admin",
            hostkey_verify=False,
        ) as m:
            pass
            # WHEN server stops
            netconf_server.stop()

    # THEN expect error
    assert str(error.value) == "Not connected to NETCONF server"
Key Auth
from pytest_netconf import NetconfServer
from ncclient import manager


def test_netconf_key_auth(
    netconf_server: NetconfServer,
):
    # GIVEN SSH username and authorized key
    netconf_server.username = "admin"
    netconf_server.authorized_key = "ssh-rsa AAAAB3NzaC1yc..."

    # WHEN connecting using key credentials
    with manager.connect(
        host="localhost",
        port=8830,
        username="admin",
        key_filename=key_filepath,
        hostkey_verify=False,
    ) as m:
        # THEN expect to be connected
        assert m.connected
Call Tracking
def test_call_tracking(netconf_server):
    # GIVEN server request and response
    handler = netconf_server.expect_request(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<nc:rpc xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="{message_id}">'
        "<nc:get-config><nc:source><nc:running/></nc:source></nc:get-config>"
        "</nc:rpc>"
    ).respond_with(
        """
        <?xml version="1.0" encoding="UTF-8"?>
        <rpc-reply message-id="{message_id}"
          xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
            <data>
                <interfaces>
                    <interface>
                        <name>eth0</name>
                    </interface>
                </interfaces>
            </data>
        </rpc-reply>
        """
    )

    # WHEN fetching rpc response from server
    with manager.connect(
        host="localhost",
        port=8830,
        username="admin",
        password="admin",
        hostkey_verify=False,
    ) as m:
        m.get_config(source="running").data_xml

    # THEN expect calls to be made
    assert netconf_server.was_called()
    assert netconf_server.get_call_count() == 1
    assert handler.was_called()
    assert handler.get_call_count() == 1

Call Tracking

Call tracking is implemented to verify received client requests and responses sent by the server.

Look at the Examples section to see how this can being used.

Server-level tracking

  • netconf_server.was_called() - Check if any requests were made
  • netconf_server.get_call_count() - Get total number of requests

Request handler tracking

  • handler.was_called() - Check if this specific request pattern was called
  • handler.get_call_count() - Get number of calls for this pattern

Versioning

Releases will follow semantic versioning (major.minor.patch). Before 1.0.0 breaking changes can be included in a minor release, therefore we highly recommend pinning this package.

Contributing

Suggest a feature or report a bug. Read our developer guide.

License

pytest-netconf is distributed under the Apache 2.0 license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_netconf-0.2.0-py3-none-any.whl (18.8 kB view details)

Uploaded Python 3

File details

Details for the file pytest_netconf-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pytest_netconf-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 18.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pytest_netconf-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8693b55eaa163bda30189bc94bdf66d35560baea7219e9fbefb50726297576a
MD5 be1c716baa42d9aecb637a323d755f37
BLAKE2b-256 227c400e6a7e39b4ca7521edf6b83bd6c6d9caf437f7190f080cadacb09ed4b2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page