CI / test (3.10) (push) Successful in 30s
CI / test (3.11) (push) Successful in 31s
CI / test (3.12) (push) Successful in 36s
CI / test (3.10) (pull_request) Successful in 40s
CI / test (3.11) (pull_request) Successful in 31s
CI / test (3.12) (pull_request) Successful in 33s
The driver accepted any SSH host key. It now verifies through napalm-device-types' HostKeyCheck (>= 4.19.0): with a known key in optional_args["ssh_host_keys"] any other key is refused before login, and every key seen is reported to optional_args["ssh_host_key_observer"]. Without either argument it connects as before (first use).
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""open() hands netmiko the host key to expect, and reports the one it saw (NetOrk/netork#1167).
|
|
|
|
OpenMediaVault and QNAP open through ``LinuxDriver.open()`` and get this too.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import paramiko
|
|
import pytest
|
|
from napalm_device_types.ssh_host_keys import (
|
|
KNOWN_KEYS_ARG,
|
|
OBSERVER_ARG,
|
|
SshHostKey,
|
|
SshHostKeyMismatch,
|
|
)
|
|
from netmiko.exceptions import NetmikoTimeoutException
|
|
|
|
from napalm_linux.linux import LinuxDriver
|
|
|
|
SERVER_KEY = paramiko.ECDSAKey.generate()
|
|
OTHER_KEY = paramiko.ECDSAKey.generate()
|
|
|
|
|
|
def _device() -> MagicMock:
|
|
device = MagicMock()
|
|
device.send_command.return_value = "/usr/bin/apt"
|
|
device.remote_conn_pre.get_transport.return_value.get_remote_server_key.return_value = SERVER_KEY
|
|
return device
|
|
|
|
|
|
def _driver(seen: list, known: SshHostKey | None = None) -> LinuxDriver:
|
|
args: dict = {"port": 2222, OBSERVER_ARG: seen.append}
|
|
if known is not None:
|
|
args[KNOWN_KEYS_ARG] = {"2222": known.as_dict()}
|
|
return LinuxDriver("10.0.0.8", "admin", "s3cret", optional_args=args)
|
|
|
|
|
|
def test_first_use_lets_netmiko_accept_and_reports_the_key():
|
|
seen: list = []
|
|
with patch("napalm_linux.linux.ConnectHandler", return_value=_device()) as handler:
|
|
_driver(seen).open()
|
|
kwargs = handler.call_args.kwargs
|
|
assert kwargs["ssh_strict"] is False
|
|
assert kwargs["alt_host_keys"] is False
|
|
assert kwargs["system_host_keys"] is False
|
|
assert seen[0]["accepted"] is True
|
|
assert seen[0]["seen"]["fingerprint"] == SshHostKey.of(SERVER_KEY).fingerprint
|
|
assert (seen[0]["host"], seen[0]["port"]) == ("10.0.0.8", 2222)
|
|
|
|
|
|
def test_a_known_key_is_the_only_one_netmiko_accepts():
|
|
seen: list = []
|
|
known = SshHostKey.of(SERVER_KEY)
|
|
files: list[str] = []
|
|
|
|
def connect(**kwargs):
|
|
files.append(kwargs["alt_key_file"])
|
|
with open(kwargs["alt_key_file"]) as f:
|
|
assert f.read().strip() == known.known_hosts_line("10.0.0.8", 2222)
|
|
assert kwargs["ssh_strict"] is True and kwargs["alt_host_keys"] is True
|
|
return _device()
|
|
|
|
with patch("napalm_linux.linux.ConnectHandler", side_effect=connect):
|
|
_driver(seen, known).open()
|
|
assert not os.path.exists(files[0])
|
|
assert seen[0]["accepted"] is True
|
|
|
|
|
|
def test_a_changed_key_is_refused():
|
|
seen: list = []
|
|
|
|
def refuse(**kwargs):
|
|
try:
|
|
raise paramiko.BadHostKeyException("10.0.0.8", SERVER_KEY, OTHER_KEY)
|
|
except paramiko.SSHException as exc: # what netmiko does with it
|
|
raise NetmikoTimeoutException(f"A paramiko SSHException occurred: {exc}")
|
|
|
|
driver = _driver(seen, SshHostKey.of(OTHER_KEY))
|
|
with patch("napalm_linux.linux.ConnectHandler", side_effect=refuse):
|
|
with pytest.raises(SshHostKeyMismatch):
|
|
driver.open()
|
|
assert driver._device is None
|
|
assert seen[0]["accepted"] is False
|