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>
This commit is contained in:
Stefan Agner 2023-11-27 23:32:11 +01:00 committed by GitHub
parent 172a7053ed
commit 9088810b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 232 additions and 41 deletions

View File

@ -6,7 +6,7 @@ from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from ..const import ( from ..const import (
DBUS_ATTR_DIAGNOSTICS, DBUS_ATTR_DIAGNOSTICS,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
@ -99,7 +99,7 @@ class OSAgent(DBusInterfaceProxy):
await asyncio.gather(*[dbus.connect(bus) for dbus in self.all]) await asyncio.gather(*[dbus.connect(bus) for dbus in self.all])
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to OS-Agent") _LOGGER.warning("Can't connect to OS-Agent")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No OS-Agent support on the host. Some Host functions have been disabled." "No OS-Agent support on the host. Some Host functions have been disabled."
) )

View File

@ -3,7 +3,7 @@ import logging
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from .const import ( from .const import (
DBUS_ATTR_CHASSIS, DBUS_ATTR_CHASSIS,
DBUS_ATTR_DEPLOYMENT, DBUS_ATTR_DEPLOYMENT,
@ -39,7 +39,7 @@ class Hostname(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-hostname") _LOGGER.warning("Can't connect to systemd-hostname")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No hostname support on the host. Hostname functions have been disabled." "No hostname support on the host. Hostname functions have been disabled."
) )

View File

@ -3,7 +3,7 @@ import logging
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND from .const import DBUS_NAME_LOGIND, DBUS_OBJECT_LOGIND
from .interface import DBusInterface from .interface import DBusInterface
from .utils import dbus_connected from .utils import dbus_connected
@ -28,8 +28,8 @@ class Logind(DBusInterface):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-logind") _LOGGER.warning("Can't connect to systemd-logind")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.info("No systemd-logind support on the host.") _LOGGER.warning("No systemd-logind support on the host.")
@dbus_connected @dbus_connected
async def reboot(self) -> None: async def reboot(self) -> None:

View File

@ -9,6 +9,8 @@ from ...exceptions import (
DBusError, DBusError,
DBusFatalError, DBusFatalError,
DBusInterfaceError, DBusInterfaceError,
DBusNoReplyError,
DBusServiceUnkownError,
HostNotSupportedError, HostNotSupportedError,
NetworkInterfaceNotFound, NetworkInterfaceNotFound,
) )
@ -143,7 +145,7 @@ class NetworkManager(DBusInterfaceProxy):
await self.settings.connect(bus) await self.settings.connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to Network Manager") _LOGGER.warning("Can't connect to Network Manager")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No Network Manager support on the host. Local network functions have been disabled." "No Network Manager support on the host. Local network functions have been disabled."
) )
@ -210,8 +212,22 @@ class NetworkManager(DBusInterfaceProxy):
# try to query it. Ignore those cases. # try to query it. Ignore those cases.
_LOGGER.debug("Can't process %s: %s", device, err) _LOGGER.debug("Can't process %s: %s", device, err)
continue continue
except (
DBusNoReplyError,
DBusServiceUnkownError,
) as err:
# This typically means that NetworkManager disappeared. Give up immeaditly.
_LOGGER.error(
"NetworkManager not responding while processing %s: %s. Giving up.",
device,
err,
)
capture_exception(err)
return
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error while processing %s: %s", device, err) _LOGGER.exception(
"Unkown error while processing %s: %s", device, err
)
capture_exception(err) capture_exception(err)
continue continue

View File

@ -12,7 +12,7 @@ from ...const import (
ATTR_PRIORITY, ATTR_PRIORITY,
ATTR_VPN, ATTR_VPN,
) )
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from ..const import ( from ..const import (
DBUS_ATTR_CONFIGURATION, DBUS_ATTR_CONFIGURATION,
DBUS_ATTR_MODE, DBUS_ATTR_MODE,
@ -67,7 +67,7 @@ class NetworkManagerDNS(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to DnsManager") _LOGGER.warning("Can't connect to DnsManager")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No DnsManager support on the host. Local DNS functions have been disabled." "No DnsManager support on the host. Local DNS functions have been disabled."
) )

View File

@ -4,7 +4,7 @@ from typing import Any
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ...exceptions import DBusError, DBusInterfaceError from ...exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS from ..const import DBUS_NAME_NM, DBUS_OBJECT_SETTINGS
from ..interface import DBusInterface from ..interface import DBusInterface
from ..network.setting import NetworkSetting from ..network.setting import NetworkSetting
@ -28,7 +28,7 @@ class NetworkManagerSettings(DBusInterface):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to Network Manager Settings") _LOGGER.warning("Can't connect to Network Manager Settings")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No Network Manager Settings support on the host. Local network functions have been disabled." "No Network Manager Settings support on the host. Local network functions have been disabled."
) )

View File

@ -4,7 +4,7 @@ from typing import Any
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from ..utils.dbus import DBusSignalWrapper from ..utils.dbus import DBusSignalWrapper
from .const import ( from .const import (
DBUS_ATTR_BOOT_SLOT, DBUS_ATTR_BOOT_SLOT,
@ -49,7 +49,7 @@ class Rauc(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to rauc") _LOGGER.warning("Can't connect to rauc")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning("Host has no rauc support. OTA updates have been disabled.") _LOGGER.warning("Host has no rauc support. OTA updates have been disabled.")
@property @property

View File

@ -5,7 +5,7 @@ import logging
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from .const import ( from .const import (
DBUS_ATTR_CACHE_STATISTICS, DBUS_ATTR_CACHE_STATISTICS,
DBUS_ATTR_CURRENT_DNS_SERVER, DBUS_ATTR_CURRENT_DNS_SERVER,
@ -59,7 +59,7 @@ class Resolved(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-resolved.") _LOGGER.warning("Can't connect to systemd-resolved.")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"Host has no systemd-resolved support. DNS will not work correctly." "Host has no systemd-resolved support. DNS will not work correctly."
) )

View File

@ -10,6 +10,7 @@ from ..exceptions import (
DBusError, DBusError,
DBusFatalError, DBusFatalError,
DBusInterfaceError, DBusInterfaceError,
DBusServiceUnkownError,
DBusSystemdNoSuchUnit, DBusSystemdNoSuchUnit,
) )
from .const import ( from .const import (
@ -86,7 +87,7 @@ class Systemd(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd") _LOGGER.warning("Can't connect to systemd")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No systemd support on the host. Host control has been disabled." "No systemd support on the host. Host control has been disabled."
) )

View File

@ -4,7 +4,7 @@ import logging
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from ..exceptions import DBusError, DBusInterfaceError from ..exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
from ..utils.dt import utc_from_timestamp from ..utils.dt import utc_from_timestamp
from .const import ( from .const import (
DBUS_ATTR_NTP, DBUS_ATTR_NTP,
@ -63,7 +63,7 @@ class TimeDate(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to systemd-timedate") _LOGGER.warning("Can't connect to systemd-timedate")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No timedate support on the host. Time/Date functions have been disabled." "No timedate support on the host. Time/Date functions have been disabled."
) )

View File

@ -6,7 +6,12 @@ from typing import Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
from dbus_fast.aio import MessageBus from dbus_fast.aio import MessageBus
from ...exceptions import DBusError, DBusInterfaceError, DBusObjectError from ...exceptions import (
DBusError,
DBusInterfaceError,
DBusObjectError,
DBusServiceUnkownError,
)
from ..const import ( from ..const import (
DBUS_ATTR_SUPPORTED_FILESYSTEMS, DBUS_ATTR_SUPPORTED_FILESYSTEMS,
DBUS_ATTR_VERSION, DBUS_ATTR_VERSION,
@ -45,7 +50,7 @@ class UDisks2(DBusInterfaceProxy):
await super().connect(bus) await super().connect(bus)
except DBusError: except DBusError:
_LOGGER.warning("Can't connect to udisks2") _LOGGER.warning("Can't connect to udisks2")
except DBusInterfaceError: except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning( _LOGGER.warning(
"No udisks2 support on the host. Host control has been disabled." "No udisks2 support on the host. Host control has been disabled."
) )

View File

@ -335,6 +335,10 @@ class DBusNotConnectedError(HostNotSupportedError):
"""D-Bus is not connected and call a method.""" """D-Bus is not connected and call a method."""
class DBusServiceUnkownError(HassioNotSupportedError):
"""D-Bus service was not available."""
class DBusInterfaceError(HassioNotSupportedError): class DBusInterfaceError(HassioNotSupportedError):
"""D-Bus interface not connected.""" """D-Bus interface not connected."""
@ -363,6 +367,10 @@ class DBusTimeoutError(DBusError):
"""D-Bus call timed out.""" """D-Bus call timed out."""
class DBusNoReplyError(DBusError):
"""D-Bus remote didn't reply/disconnected."""
class DBusFatalError(DBusError): class DBusFatalError(DBusError):
"""D-Bus call going wrong. """D-Bus call going wrong.

View File

@ -15,18 +15,21 @@ from dbus_fast import (
) )
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.aio.proxy_object import ProxyInterface, ProxyObject from dbus_fast.aio.proxy_object import ProxyInterface, ProxyObject
from dbus_fast.errors import DBusError from dbus_fast.errors import DBusError as DBusFastDBusError
from dbus_fast.introspection import Node from dbus_fast.introspection import Node
from ..exceptions import ( from ..exceptions import (
DBusError,
DBusFatalError, DBusFatalError,
DBusInterfaceError, DBusInterfaceError,
DBusInterfaceMethodError, DBusInterfaceMethodError,
DBusInterfacePropertyError, DBusInterfacePropertyError,
DBusInterfaceSignalError, DBusInterfaceSignalError,
DBusNoReplyError,
DBusNotConnectedError, DBusNotConnectedError,
DBusObjectError, DBusObjectError,
DBusParseError, DBusParseError,
DBusServiceUnkownError,
DBusTimeoutError, DBusTimeoutError,
HassioNotSupportedError, HassioNotSupportedError,
) )
@ -62,9 +65,11 @@ class DBus:
return self return self
@staticmethod @staticmethod
def from_dbus_error(err: DBusError) -> HassioNotSupportedError | DBusError: def from_dbus_error(err: DBusFastDBusError) -> HassioNotSupportedError | DBusError:
"""Return correct dbus error based on type.""" """Return correct dbus error based on type."""
if err.type in {ErrorType.SERVICE_UNKNOWN, ErrorType.UNKNOWN_INTERFACE}: if err.type == ErrorType.SERVICE_UNKNOWN:
return DBusServiceUnkownError(err.text)
if err.type == ErrorType.UNKNOWN_INTERFACE:
return DBusInterfaceError(err.text) return DBusInterfaceError(err.text)
if err.type in { if err.type in {
ErrorType.UNKNOWN_METHOD, ErrorType.UNKNOWN_METHOD,
@ -80,6 +85,8 @@ class DBus:
return DBusNotConnectedError(err.text) return DBusNotConnectedError(err.text)
if err.type == ErrorType.TIMEOUT: if err.type == ErrorType.TIMEOUT:
return DBusTimeoutError(err.text) return DBusTimeoutError(err.text)
if err.type == ErrorType.NO_REPLY:
return DBusNoReplyError(err.text)
return DBusFatalError(err.text, type_=err.type) return DBusFatalError(err.text, type_=err.type)
@staticmethod @staticmethod
@ -102,7 +109,7 @@ class DBus:
*args, unpack_variants=True *args, unpack_variants=True
) )
return await getattr(proxy_interface, method)(*args) return await getattr(proxy_interface, method)(*args)
except DBusError as err: except DBusFastDBusError as err:
raise DBus.from_dbus_error(err) raise DBus.from_dbus_error(err)
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
capture_exception(err) capture_exception(err)
@ -126,6 +133,8 @@ class DBus:
raise DBusParseError( raise DBusParseError(
f"Can't parse introspect data: {err}", _LOGGER.error f"Can't parse introspect data: {err}", _LOGGER.error
) from err ) from err
except DBusFastDBusError as err:
raise DBus.from_dbus_error(err)
except (EOFError, TimeoutError): except (EOFError, TimeoutError):
_LOGGER.warning( _LOGGER.warning(
"Busy system at %s - %s", self.bus_name, self.object_path "Busy system at %s - %s", self.bus_name, self.object_path

View File

@ -5,11 +5,12 @@ import pytest
from supervisor.dbus.agent import OSAgent from supervisor.dbus.agent import OSAgent
from tests.common import mock_dbus_services
from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.os_agent import OSAgent as OSAgentService from tests.dbus_service_mocks.os_agent import OSAgent as OSAgentService
@pytest.fixture(name="os_agent_service", autouse=True) @pytest.fixture(name="os_agent_service")
async def fixture_os_agent_service( async def fixture_os_agent_service(
os_agent_services: dict[str, DBusServiceMock] os_agent_services: dict[str, DBusServiceMock]
) -> OSAgentService: ) -> OSAgentService:
@ -39,3 +40,36 @@ async def test_dbus_osagent(
await os_agent_service.ping() await os_agent_service.ping()
await os_agent_service.ping() await os_agent_service.ping()
assert os_agent.diagnostics is True assert os_agent.diagnostics is True
@pytest.mark.parametrize(
"skip_service",
[
"os_agent",
"agent_apparmor",
"agent_datadisk",
],
)
async def test_dbus_osagent_connect_error(
skip_service: str, dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test OS Agent errors during connect."""
os_agent_services = {
"os_agent": None,
"agent_apparmor": None,
"agent_cgroup": None,
"agent_datadisk": None,
"agent_system": None,
"agent_boards": None,
"agent_boards_yellow": None,
}
os_agent_services.pop(skip_service)
await mock_dbus_services(
os_agent_services,
dbus_session_bus,
)
os_agent = OSAgent()
await os_agent.connect(dbus_session_bus)
assert "No OS-Agent support on the host" in caplog.text

View File

@ -12,7 +12,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.network_dns_manager import DnsManager as DnsManagerService from tests.dbus_service_mocks.network_dns_manager import DnsManager as DnsManagerService
@pytest.fixture(name="dns_manager_service", autouse=True) @pytest.fixture(name="dns_manager_service")
async def fixture_dns_manager_service( async def fixture_dns_manager_service(
dbus_session_bus: MessageBus, dbus_session_bus: MessageBus,
) -> DnsManagerService: ) -> DnsManagerService:
@ -49,3 +49,12 @@ async def test_dns(
await dns_manager_service.ping() await dns_manager_service.ping()
await dns_manager_service.ping() await dns_manager_service.ping()
assert dns_manager.mode == "default" assert dns_manager.mode == "default"
async def test_dbus_dns_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to dns error."""
dns_manager = NetworkManagerDNS()
await dns_manager.connect(dbus_session_bus)
assert "No DnsManager support on the host" in caplog.text

View File

@ -9,7 +9,12 @@ import pytest
from supervisor.dbus.const import ConnectionStateType from supervisor.dbus.const import ConnectionStateType
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface from supervisor.dbus.network.interface import NetworkInterface
from supervisor.exceptions import DBusFatalError, DBusParseError, HostNotSupportedError from supervisor.exceptions import (
DBusFatalError,
DBusParseError,
DBusServiceUnkownError,
HostNotSupportedError,
)
from supervisor.utils.dbus import DBus from supervisor.utils.dbus import DBus
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN
@ -20,7 +25,7 @@ from tests.dbus_service_mocks.network_manager import (
) )
@pytest.fixture(name="network_manager_service", autouse=True) @pytest.fixture(name="network_manager_service")
async def fixture_network_manager_service( async def fixture_network_manager_service(
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]], network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> NetworkManagerService: ) -> NetworkManagerService:
@ -134,6 +139,7 @@ async def test_removed_devices_disconnect(
async def test_handling_bad_devices( async def test_handling_bad_devices(
network_manager_service: NetworkManagerService,
network_manager: NetworkManager, network_manager: NetworkManager,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
capture_exception: Mock, capture_exception: Mock,
@ -161,7 +167,7 @@ async def test_handling_bad_devices(
await network_manager.update( await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]} {"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]}
) )
assert f"Error while processing {device}" in caplog.text assert f"Unkown error while processing {device}" in caplog.text
capture_exception.assert_called_once_with(err) capture_exception.assert_called_once_with(err)
# We should be able to debug these situations if necessary # We should be able to debug these situations if necessary
@ -207,3 +213,37 @@ async def test_ignore_veth_only_changes(
) )
await network_manager_service.ping() await network_manager_service.ping()
connect.assert_called_once() 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

View File

@ -11,7 +11,7 @@ from tests.dbus_service_mocks.network_connection_settings import SETTINGS_FIXTUR
from tests.dbus_service_mocks.network_settings import Settings as SettingsService from tests.dbus_service_mocks.network_settings import Settings as SettingsService
@pytest.fixture(name="settings_service", autouse=True) @pytest.fixture(name="settings_service")
async def fixture_settings_service(dbus_session_bus: MessageBus) -> SettingsService: async def fixture_settings_service(dbus_session_bus: MessageBus) -> SettingsService:
"""Mock Settings service.""" """Mock Settings service."""
yield ( yield (
@ -55,3 +55,12 @@ async def test_reload_connections(
assert await settings.reload_connections() is True assert await settings.reload_connections() is True
assert settings_service.ReloadConnections.calls == [tuple()] assert settings_service.ReloadConnections.calls == [tuple()]
async def test_dbus_network_settings_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to network settings error."""
settings = NetworkManagerSettings()
await settings.connect(dbus_session_bus)
assert "No Network Manager Settings support on the host" in caplog.text

View File

@ -10,7 +10,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.hostname import Hostname as HostnameService from tests.dbus_service_mocks.hostname import Hostname as HostnameService
@pytest.fixture(name="hostname_service", autouse=True) @pytest.fixture(name="hostname_service")
async def fixture_hostname_service(dbus_session_bus: MessageBus) -> HostnameService: async def fixture_hostname_service(dbus_session_bus: MessageBus) -> HostnameService:
"""Mock hostname dbus service.""" """Mock hostname dbus service."""
yield (await mock_dbus_services({"hostname": None}, dbus_session_bus))["hostname"] yield (await mock_dbus_services({"hostname": None}, dbus_session_bus))["hostname"]
@ -61,3 +61,12 @@ async def test_dbus_sethostname(
assert hostname_service.SetStaticHostname.calls == [("StarWars", False)] assert hostname_service.SetStaticHostname.calls == [("StarWars", False)]
await hostname_service.ping() await hostname_service.ping()
assert hostname.hostname == "StarWars" assert hostname.hostname == "StarWars"
async def test_dbus_hostname_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to hostname error."""
hostname = Hostname()
await hostname.connect(dbus_session_bus)
assert "No hostname support on the host" in caplog.text

View File

@ -10,7 +10,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.logind import Logind as LogindService from tests.dbus_service_mocks.logind import Logind as LogindService
@pytest.fixture(name="logind_service", autouse=True) @pytest.fixture(name="logind_service")
async def fixture_logind_service(dbus_session_bus: MessageBus) -> LogindService: async def fixture_logind_service(dbus_session_bus: MessageBus) -> LogindService:
"""Mock logind dbus service.""" """Mock logind dbus service."""
yield (await mock_dbus_services({"logind": None}, dbus_session_bus))["logind"] yield (await mock_dbus_services({"logind": None}, dbus_session_bus))["logind"]
@ -42,3 +42,12 @@ async def test_power_off(logind_service: LogindService, dbus_session_bus: Messag
assert await logind.power_off() is None assert await logind.power_off() is None
assert logind_service.PowerOff.calls == [(False,)] assert logind_service.PowerOff.calls == [(False,)]
async def test_dbus_logind_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to logind error."""
logind = Logind()
await logind.connect(dbus_session_bus)
assert "No systemd-logind support on the host" in caplog.text

View File

@ -11,7 +11,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.rauc import Rauc as RaucService from tests.dbus_service_mocks.rauc import Rauc as RaucService
@pytest.fixture(name="rauc_service", autouse=True) @pytest.fixture(name="rauc_service")
async def fixture_rauc_service(dbus_session_bus: MessageBus) -> RaucService: async def fixture_rauc_service(dbus_session_bus: MessageBus) -> RaucService:
"""Mock rauc dbus service.""" """Mock rauc dbus service."""
yield (await mock_dbus_services({"rauc": None}, dbus_session_bus))["rauc"] yield (await mock_dbus_services({"rauc": None}, dbus_session_bus))["rauc"]
@ -41,7 +41,7 @@ async def test_rauc_info(rauc_service: RaucService, dbus_session_bus: MessageBus
assert rauc.last_error == "" assert rauc.last_error == ""
async def test_install(dbus_session_bus: MessageBus): async def test_install(rauc_service: RaucService, dbus_session_bus: MessageBus):
"""Test install.""" """Test install."""
rauc = Rauc() rauc = Rauc()
@ -55,7 +55,7 @@ async def test_install(dbus_session_bus: MessageBus):
assert await signal.wait_for_signal() == [0] assert await signal.wait_for_signal() == [0]
async def test_get_slot_status(dbus_session_bus: MessageBus): async def test_get_slot_status(rauc_service: RaucService, dbus_session_bus: MessageBus):
"""Test get slot status.""" """Test get slot status."""
rauc = Rauc() rauc = Rauc()
@ -76,7 +76,7 @@ async def test_get_slot_status(dbus_session_bus: MessageBus):
assert slot_status[4][1]["bootname"] == "B" assert slot_status[4][1]["bootname"] == "B"
async def test_mark(dbus_session_bus: MessageBus): async def test_mark(rauc_service: RaucService, dbus_session_bus: MessageBus):
"""Test mark.""" """Test mark."""
rauc = Rauc() rauc = Rauc()
@ -88,3 +88,12 @@ async def test_mark(dbus_session_bus: MessageBus):
mark = await rauc.mark(RaucState.GOOD, "booted") mark = await rauc.mark(RaucState.GOOD, "booted")
assert mark[0] == "kernel.1" assert mark[0] == "kernel.1"
assert mark[1] == "marked slot kernel.1 as good" assert mark[1] == "marked slot kernel.1 as good"
async def test_dbus_rauc_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to rauc error."""
rauc = Rauc()
await rauc.connect(dbus_session_bus)
assert "Host has no rauc support" in caplog.text

View File

@ -19,7 +19,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.resolved import Resolved as ResolvedService from tests.dbus_service_mocks.resolved import Resolved as ResolvedService
@pytest.fixture(name="resolved_service", autouse=True) @pytest.fixture(name="resolved_service")
async def fixture_resolved_service(dbus_session_bus: MessageBus) -> ResolvedService: async def fixture_resolved_service(dbus_session_bus: MessageBus) -> ResolvedService:
"""Mock resolved dbus service.""" """Mock resolved dbus service."""
yield (await mock_dbus_services({"resolved": None}, dbus_session_bus))["resolved"] yield (await mock_dbus_services({"resolved": None}, dbus_session_bus))["resolved"]
@ -102,3 +102,12 @@ async def test_dbus_resolved_info(
await resolved_service.ping() await resolved_service.ping()
await resolved_service.ping() # To process the follow-up get all properties call await resolved_service.ping() # To process the follow-up get all properties call
assert resolved.llmnr_hostname == "homeassistant" assert resolved.llmnr_hostname == "homeassistant"
async def test_dbus_resolved_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to resolved error."""
resolved = Resolved()
await resolved.connect(dbus_session_bus)
assert "Host has no systemd-resolved support" in caplog.text

View File

@ -12,7 +12,7 @@ from tests.common import mock_dbus_services
from tests.dbus_service_mocks.timedate import TimeDate as TimeDateService from tests.dbus_service_mocks.timedate import TimeDate as TimeDateService
@pytest.fixture(name="timedate_service", autouse=True) @pytest.fixture(name="timedate_service")
async def fixture_timedate_service(dbus_session_bus: MessageBus) -> TimeDateService: async def fixture_timedate_service(dbus_session_bus: MessageBus) -> TimeDateService:
"""Mock timedate dbus service.""" """Mock timedate dbus service."""
yield (await mock_dbus_services({"timedate": None}, dbus_session_bus))["timedate"] yield (await mock_dbus_services({"timedate": None}, dbus_session_bus))["timedate"]
@ -81,3 +81,12 @@ async def test_dbus_setntp(
assert timedate_service.SetNTP.calls == [(False, False)] assert timedate_service.SetNTP.calls == [(False, False)]
await timedate_service.ping() await timedate_service.ping()
assert timedate.ntp is False assert timedate.ntp is False
async def test_dbus_timedate_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):
"""Test connecting to timedate error."""
timedate = TimeDate()
await timedate.connect(dbus_session_bus)
assert "No timedate support on the host" in caplog.text

View File

@ -2,12 +2,18 @@
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
from dbus_fast import ErrorType
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.errors import DBusError as DBusFastDBusError
from dbus_fast.service import method, signal from dbus_fast.service import method, signal
import pytest import pytest
from supervisor.dbus.const import DBUS_OBJECT_BASE from supervisor.dbus.const import DBUS_OBJECT_BASE
from supervisor.exceptions import DBusFatalError, DBusInterfaceError from supervisor.exceptions import (
DBusFatalError,
DBusInterfaceError,
DBusServiceUnkownError,
)
from supervisor.utils.dbus import DBus from supervisor.utils.dbus import DBus
from tests.common import load_fixture from tests.common import load_fixture
@ -172,3 +178,12 @@ async def test_init_proxy(test_service: TestInterface, dbus_session_bus: Message
test_service.signal_test() test_service.signal_test()
await test_service.ping() await test_service.ping()
assert callback_count == 0 assert callback_count == 0
def test_from_dbus_error():
"""Test converting DBus fast errors to Supervisor specific errors."""
dbus_fast_error = DBusFastDBusError(
ErrorType.SERVICE_UNKNOWN, "The name is not activatable"
)
assert type(DBus.from_dbus_error(dbus_fast_error)) is DBusServiceUnkownError