# pytest-netconf
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nomios-opensource/pytest-netconf/publish.yml)
![Codecov](https://img.shields.io/codecov/c/github/nomios-opensource/pytest-netconf)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-netconf)
![PyPI - Downloads](https://img.shields.io/pypi/dm/pytest-netconf)
![GitHub License](https://img.shields.io/github/license/nomios-opensource/pytest-netconf)
A pytest plugin that provides a mock NETCONF (RFC6241/RFC6242) server for local testing.
`pytest-netconf` is authored by [Adam Kirchberger](https://github.com/adamkirchberger), governed as a [benevolent dictatorship](CODE_OF_CONDUCT.md), and distributed under [license](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:
## 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](#examples)
```python
# 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
<details>
<summary>Get Config</summary>
<br>
```python
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
)
```
</details>
<details>
<summary>Authentication Fail</summary>
<br>
```python
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
```
</details>
<details>
<summary>Custom Capabilities</summary>
<br>
```python
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
```
</details>
<details>
<summary>Server Disconnect</summary>
<br>
```python
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"
```
</details>
<details>
<summary>Key Auth</summary>
<br>
```python
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
```
</details>
## 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](CONTRIBUTING.md).
## License
pytest-netconf is distributed under the Apache 2.0 [license](LICENSE).
Raw data
{
"_id": null,
"home_page": null,
"name": "pytest-netconf",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "Netconf, Network automation, Network engineering, Network testing",
"author": "Adam Kirchberger",
"author_email": "adam.kirchberger@nomios.co.uk",
"download_url": null,
"platform": null,
"description": "# pytest-netconf\n\n![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nomios-opensource/pytest-netconf/publish.yml)\n![Codecov](https://img.shields.io/codecov/c/github/nomios-opensource/pytest-netconf) \n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-netconf)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/pytest-netconf)\n![GitHub License](https://img.shields.io/github/license/nomios-opensource/pytest-netconf)\n\nA pytest plugin that provides a mock NETCONF (RFC6241/RFC6242) server for local testing. \n\n`pytest-netconf` is authored by [Adam Kirchberger](https://github.com/adamkirchberger), governed as a [benevolent dictatorship](CODE_OF_CONDUCT.md), and distributed under [license](LICENSE).\n\n## Introduction\n\nTesting 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.\n\n## Features\n\n- **NETCONF server**, a real SSH server is run locally which enables testing using actual network connections instead of patching.\n- **Predefined requests and responses**, define specific NETCONF requests and responses to meet your testing needs.\n- **Capability testing**, define specific capabilities you want the server to support and test their responses.\n- **Authentication testing**, test error handling for authentication issues (supports password or key auth).\n- **Connection testing**, test error handling when tearing down connections unexpectedly.\n\n## NETCONF Clients\n\nThe clients below have been tested\n\n- `ncclient` :white_check_mark:\n- `netconf-client` :white_check_mark:\n- `scrapli-netconf` :white_check_mark:\n\n## Quickstart\n\nThe 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.\n\nFor more use cases see [examples](#examples)\n\n\n```python\n#\u00a0Configure server settings\nnetconf_server.username = None #\u00a0allow any username\nnetconf_server.password = None # allow any password\nnetconf_server.port = 8830 #\u00a0default value\n\n#\u00a0Configure a request and response\nnetconf_server.expect_request(\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n '<nc:rpc xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\"{message_id}\">'\n \"<nc:get-config><nc:source><nc:running/></nc:source></nc:get-config>\"\n \"</nc:rpc>\"\n ).respond_with(\n \"\"\"\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <rpc-reply message-id=\"{message_id}\"\n xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n <data>\n <interfaces>\n <interface>\n <name>eth0</name>\n </interface>\n </interfaces>\n </data>\n </rpc-reply>\n \"\"\"\n )\n```\n\n## Examples\n\n<details>\n<summary>Get Config</summary>\n<br>\n\n```python\nfrom pytest_netconf import NetconfServer\nfrom ncclient import manager\n\n\ndef test_netconf_get_config(\n netconf_server: NetconfServer,\n):\n # GIVEN server request and response\n netconf_server.expect_request(\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n '<nc:rpc xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\" message-id=\"{message_id}\">'\n \"<nc:get-config><nc:source><nc:running/></nc:source></nc:get-config>\"\n \"</nc:rpc>\"\n ).respond_with(\n \"\"\"\n <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <rpc-reply message-id=\"{message_id}\"\n xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n <data>\n <interfaces>\n <interface>\n <name>eth0</name>\n </interface>\n </interfaces>\n </data>\n </rpc-reply>\"\"\"\n )\n\n # WHEN fetching rpc response from server\n with manager.connect(\n host=\"localhost\",\n port=8830,\n username=\"admin\",\n password=\"admin\",\n hostkey_verify=False,\n ) as m:\n response = m.get_config(source=\"running\").data_xml\n\n # THEN expect response\n assert (\n \"\"\"\n <interfaces>\n <interface>\n <name>eth0</name>\n </interface>\n </interfaces>\n \"\"\"\n in response\n )\n```\n</details>\n\n<details>\n<summary>Authentication Fail</summary>\n<br>\n\n```python\nfrom pytest_netconf import NetconfServer\nfrom ncclient import manager\nfrom ncclient.transport.errors import AuthenticationError\n\n\ndef test_netconf_auth_fail(\n netconf_server: NetconfServer,\n):\n # GIVEN username and password have been defined\n netconf_server.username = \"admin\"\n netconf_server.password = \"password\"\n\n # WHEN connecting using wrong credentials\n with pytest.raises(AuthenticationError) as error:\n with manager.connect(\n host=\"localhost\",\n port=8830,\n username=\"foo\",\n password=\"bar\",\n hostkey_verify=False,\n ):\n ...\n\n # THEN expect error\n assert error\n```\n</details>\n\n<details>\n<summary>Custom Capabilities</summary>\n<br>\n\n```python\nfrom pytest_netconf import NetconfServer\nfrom ncclient import manager\n\n\ndef test_netconf_capabilities(\n netconf_server: NetconfServer,\n):\n # GIVEN extra capabilities\n netconf_server.capabilities.append(\"urn:ietf:params:netconf:capability:foo:1.1\")\n netconf_server.capabilities.append(\"urn:ietf:params:netconf:capability:bar:1.1\")\n\n # WHEN receiving server capabilities\n with manager.connect(\n host=\"localhost\",\n port=8830,\n username=\"admin\",\n password=\"admin\",\n hostkey_verify=False,\n ) as m:\n server_capabilities = m.server_capabilities\n\n # THEN expect to see capabilities\n assert \"urn:ietf:params:netconf:capability:foo:1.1\" in server_capabilities\n assert \"urn:ietf:params:netconf:capability:bar:1.1\" in server_capabilities\n```\n</details>\n\n<details>\n<summary>Server Disconnect</summary>\n<br>\n\n```python\nfrom pytest_netconf import NetconfServer\nfrom ncclient import manager\nfrom ncclient.transport.errors import TransportError\n\n\ndef test_netconf_server_disconnect(\n netconf_server: NetconfServer,\n):\n # GIVEN netconf connection\n with pytest.raises(TransportError) as error:\n with manager.connect(\n host=\"localhost\",\n port=8830,\n username=\"admin\",\n password=\"admin\",\n hostkey_verify=False,\n ) as m:\n pass\n # WHEN server stops\n netconf_server.stop()\n\n # THEN expect error\n assert str(error.value) == \"Not connected to NETCONF server\"\n```\n</details>\n\n<details>\n<summary>Key Auth</summary>\n<br>\n\n```python\nfrom pytest_netconf import NetconfServer\nfrom ncclient import manager\n\n\ndef test_netconf_key_auth(\n netconf_server: NetconfServer,\n):\n # GIVEN SSH username and authorized key\n netconf_server.username = \"admin\"\n netconf_server.authorized_key = \"ssh-rsa AAAAB3NzaC1yc...\"\n\n # WHEN connecting using key credentials\n with manager.connect(\n host=\"localhost\",\n port=8830,\n username=\"admin\",\n key_filename=key_filepath,\n hostkey_verify=False,\n ) as m:\n # THEN expect to be connected\n assert m.connected\n```\n</details>\n\n\n## Versioning\n\nReleases 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.\n\n## Contributing\n\nSuggest a [feature]() or report a [bug](). Read our developer [guide](CONTRIBUTING.md).\n\n## License\n\npytest-netconf is distributed under the Apache 2.0 [license](LICENSE).\n\n",
"bugtrack_url": null,
"license": "Apache-2.0",
"summary": "A pytest plugin that provides a mock NETCONF (RFC6241/RFC6242) server for local testing.",
"version": "0.1.0",
"project_urls": null,
"split_keywords": [
"netconf",
" network automation",
" network engineering",
" network testing"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "139d04ff62a17289763b87957ff6dc08bbc79930c12493df964e78f490999cf3",
"md5": "7888f0ee91adad543aad75e287e0f558",
"sha256": "8b91ce02b17b6058dfbb17e3d5532677e55b16fb01f40ff6a988a72d00f487ba"
},
"downloads": -1,
"filename": "pytest_netconf-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "7888f0ee91adad543aad75e287e0f558",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 17813,
"upload_time": "2024-08-08T09:55:04",
"upload_time_iso_8601": "2024-08-08T09:55:04.278533Z",
"url": "https://files.pythonhosted.org/packages/13/9d/04ff62a17289763b87957ff6dc08bbc79930c12493df964e78f490999cf3/pytest_netconf-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-08 09:55:04",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "pytest-netconf"
}