supervisor/tests/dbus/network/test_network_manager.py
Stefan Agner 9088810b49
Improve D-Bus error handling for NetworkManager (#4720)
* Improve D-Bus error handling for NetworkManager

There are quite some errors captured which are related by seemingly a
suddenly missing NetworkManager. Errors appear as:
23-11-21 17:42:50 ERROR (MainThread) [supervisor.dbus.network] Error while processing /org/freedesktop/NetworkManager/Devices/10: Remote peer disconnected
...
23-11-21 17:42:50 ERROR (MainThread) [supervisor.dbus.network] Error while processing /org/freedesktop/NetworkManager/Devices/35: The name is not activatable

Both errors seem to already happen at introspection time, however
the current code doesn't converts these errors to Supervisor issues.
This PR uses the already existing `DBus.from_dbus_error()`.

Furthermore this adds a new Exception `DBusNoReplyError` for the
`ErrorType.NO_REPLY` (or `org.freedesktop.DBus.Error.NoReply` in
D-Bus terms, which is the type of the first of the two issues above).

And finally it separates the `ErrorType.SERVICE_UNKNOWN` (or
`org.freedesktop.DBus.Error.ServiceUnknown` in D-Bus terms, which is
the second of the above issue) from `DBusInterfaceError` into a new
`DBusServiceUnkownError`.

This allows to handle errors more specifically.

To avoid too much churn, all instances where `DBusInterfaceError`
got handled, we are now also handling `DBusServiceUnkownError`.

The `DBusNoReplyError` and `DBusServiceUnkownError` appear when
the NetworkManager service stops or crashes. Instead of retrying
every interface we know, just give up if one of these issues appear.
This should significantly lower error messages users are seeing
and Sentry events.

* Remove unnecessary statement

* Fix pytests

* Make sure error strings are compared correctly

* Fix typo/remove unnecessary pylint exception

* Fix DBusError typing

* Add pytest for from_dbus_error

* Revert "Make sure error strings are compared correctly"

This reverts commit 10dc2e4c3887532921414b4291fe3987186db408.

* Add test cases

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2023-11-27 23:32:11 +01:00

250 lines
9.2 KiB
Python

"""Test NetworkInterface."""
import logging
from unittest.mock import Mock, PropertyMock, patch
from dbus_fast.aio.message_bus import MessageBus
import pytest
from supervisor.dbus.const import ConnectionStateType
from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface
from supervisor.exceptions import (
DBusFatalError,
DBusParseError,
DBusServiceUnkownError,
HostNotSupportedError,
)
from supervisor.utils.dbus import DBus
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import SETTINGS_FIXTURE
from tests.dbus_service_mocks.network_manager import (
NetworkManager as NetworkManagerService,
)
@pytest.fixture(name="network_manager_service")
async def fixture_network_manager_service(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> NetworkManagerService:
"""Mock NetworkManager dbus service."""
yield network_manager_services["network_manager"]
async def test_network_manager(
network_manager_service: NetworkManagerService, dbus_session_bus: MessageBus
):
"""Test network manager update."""
network_manager = NetworkManager()
assert network_manager.connectivity_enabled is None
await network_manager.connect(dbus_session_bus)
assert TEST_INTERFACE in network_manager
assert network_manager.connectivity_enabled is True
network_manager_service.emit_properties_changed({"ConnectivityCheckEnabled": False})
await network_manager_service.ping()
assert network_manager.connectivity_enabled is False
network_manager_service.emit_properties_changed({}, ["ConnectivityCheckEnabled"])
await network_manager_service.ping()
await network_manager_service.ping()
assert network_manager.connectivity_enabled is True
async def test_network_manager_version(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test if version validate work."""
await network_manager._validate_version()
assert network_manager.version == "1.22.10"
network_manager_service.version = "1.13.9"
with pytest.raises(HostNotSupportedError):
await network_manager._validate_version()
assert network_manager.version == "1.13.9"
async def test_check_connectivity(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test connectivity check."""
network_manager_service.CheckConnectivity.calls.clear()
assert await network_manager.check_connectivity() == 4
assert network_manager_service.CheckConnectivity.calls == []
assert await network_manager.check_connectivity(force=True) == 4
assert network_manager_service.CheckConnectivity.calls == [tuple()]
async def test_activate_connection(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test activate connection."""
network_manager_service.ActivateConnection.calls.clear()
connection = await network_manager.activate_connection(
"/org/freedesktop/NetworkManager/Settings/1",
"/org/freedesktop/NetworkManager/Devices/1",
)
assert connection.state == ConnectionStateType.ACTIVATED
assert (
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
)
assert network_manager_service.ActivateConnection.calls == [
(
"/org/freedesktop/NetworkManager/Settings/1",
"/org/freedesktop/NetworkManager/Devices/1",
"/",
)
]
async def test_add_and_activate_connection(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test add and activate connection."""
network_manager_service.AddAndActivateConnection.calls.clear()
settings, connection = await network_manager.add_and_activate_connection(
SETTINGS_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1"
)
assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto"
assert connection.state == ConnectionStateType.ACTIVATED
assert (
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
)
assert network_manager_service.AddAndActivateConnection.calls == [
(SETTINGS_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1", "/")
]
async def test_removed_devices_disconnect(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Test removed devices are disconnected."""
wlan = network_manager.get(TEST_INTERFACE_WLAN)
assert wlan.is_connected is True
network_manager_service.emit_properties_changed({"Devices": []})
await network_manager_service.ping()
assert TEST_INTERFACE_WLAN not in network_manager
assert wlan.is_connected is False
async def test_handling_bad_devices(
network_manager_service: NetworkManagerService,
network_manager: NetworkManager,
caplog: pytest.LogCaptureFixture,
capture_exception: Mock,
):
"""Test handling of bad and disappearing devices."""
caplog.clear()
caplog.set_level(logging.INFO, "supervisor.dbus.network")
with patch.object(DBus, "init_proxy", side_effect=DBusFatalError()):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/100"]}
)
assert f"Can't process {device}" not in caplog.text
await network_manager.update()
with patch.object(DBus, "properties", new=PropertyMock(return_value=None)):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/101"]}
)
assert f"Can't process {device}" not in caplog.text
# Unparseable introspections shouldn't happen, this one is logged and captured
await network_manager.update()
with patch.object(DBus, "init_proxy", side_effect=(err := DBusParseError())):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]}
)
assert f"Unkown error while processing {device}" in caplog.text
capture_exception.assert_called_once_with(err)
# We should be able to debug these situations if necessary
caplog.set_level(logging.DEBUG, "supervisor.dbus.network")
await network_manager.update()
with patch.object(DBus, "init_proxy", side_effect=DBusFatalError()):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/103"]}
)
assert f"Can't process {device}" in caplog.text
await network_manager.update()
with patch.object(DBus, "properties", new=PropertyMock(return_value=None)):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/104"]}
)
assert f"Can't process {device}" in caplog.text
async def test_ignore_veth_only_changes(
network_manager_service: NetworkManagerService, network_manager: NetworkManager
):
"""Changes to list of devices is ignored unless it changes managed devices."""
assert network_manager.properties["Devices"] == [
"/org/freedesktop/NetworkManager/Devices/1",
"/org/freedesktop/NetworkManager/Devices/3",
]
with patch.object(NetworkInterface, "connect") as connect:
network_manager_service.emit_properties_changed(
{
"Devices": [
"/org/freedesktop/NetworkManager/Devices/1",
"/org/freedesktop/NetworkManager/Devices/3",
"/org/freedesktop/NetworkManager/Devices/35",
]
}
)
await network_manager_service.ping()
connect.assert_not_called()
network_manager_service.emit_properties_changed(
{"Devices": ["/org/freedesktop/NetworkManager/Devices/35"]}
)
await network_manager_service.ping()
connect.assert_called_once()
async def test_network_manager_stopped(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
network_manager: NetworkManager,
dbus_session_bus: MessageBus,
caplog: pytest.LogCaptureFixture,
capture_exception: Mock,
):
"""Test network manager stopped and dbus service no longer accessible."""
services = list(network_manager_services.values())
while services:
service = services.pop(0)
if isinstance(service, dict):
services.extend(service.values())
else:
dbus_session_bus.unexport(service.object_path, service)
await dbus_session_bus.release_name("org.freedesktop.NetworkManager")
assert network_manager.is_connected is True
await network_manager.update(
{
"Devices": [
"/org/freedesktop/NetworkManager/Devices/9",
"/org/freedesktop/NetworkManager/Devices/15",
"/org/freedesktop/NetworkManager/Devices/20",
"/org/freedesktop/NetworkManager/Devices/35",
]
}
)
capture_exception.assert_called_once()
assert isinstance(capture_exception.call_args.args[0], DBusServiceUnkownError)
assert "NetworkManager not responding" in caplog.text