diff --git a/setup.py b/setup.py index 953e5f119..ad904c3a2 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( "supervisor.docker", "supervisor.homeassistant", "supervisor.host", - "supervisor.job", + "supervisor.jobs", "supervisor.misc", "supervisor.plugins", "supervisor.resolution.evaluations", diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 35432749e..56676c7b2 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -10,7 +10,7 @@ import sentry_sdk from sentry_sdk.integrations.aiohttp import AioHttpIntegration from sentry_sdk.integrations.logging import LoggingIntegration -from supervisor.job import JobManager +from supervisor.jobs import JobManager from .addons import AddonManager from .api import RestAPI diff --git a/supervisor/core.py b/supervisor/core.py index 9ee31a435..124b8e3fd 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -57,9 +57,6 @@ class Core(CoreSysAttributes): # Load information from container await self.sys_supervisor.load() - # Check internet on startup - await self.sys_supervisor.check_connectivity() - # Evaluate the system await self.sys_resolution.evaluate.evaluate_system() @@ -140,6 +137,9 @@ class Core(CoreSysAttributes): "System running in a unhealthy state and need manual intervention!" ) + # Check internet on startup + await self.sys_supervisor.check_connectivity() + # Mark booted partition as healthy if self.sys_hassos.available: await self.sys_hassos.mark_healthy() diff --git a/supervisor/coresys.py b/supervisor/coresys.py index bc3ed3674..60db6a3bf 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from .homeassistant import HomeAssistant from .host import HostManager from .ingress import Ingress - from .job import JobManager + from .jobs import JobManager from .misc.hwmon import HwMonitor from .misc.scheduler import Scheduler from .misc.tasks import Tasks diff --git a/supervisor/host/network.py b/supervisor/host/network.py index 06e5c8cfe..dd6fdc47a 100644 --- a/supervisor/host/network.py +++ b/supervisor/host/network.py @@ -75,7 +75,8 @@ class NetworkManager(CoreSysAttributes): try: state = await self.sys_dbus.network.check_connectivity() self._connectivity = state[0] == 4 - except DBusError: + except DBusError as err: + _LOGGER.warning("Can't update connectivity information: %s", err) self._connectivity = False def get(self, inet_name: str) -> Interface: @@ -95,9 +96,11 @@ class NetworkManager(CoreSysAttributes): except DBusError: _LOGGER.warning("Can't update network information!") except DBusNotConnectedError as err: - _LOGGER.error("No hostname D-Bus connection available") + _LOGGER.error("No network D-Bus connection available") raise HostNotSupportedError() from err + await self.check_connectivity() + async def apply_changes(self, interface: Interface) -> None: """Apply Interface changes to host.""" inet = self.sys_dbus.network.interfaces.get(interface.name) diff --git a/supervisor/job/__init__.py b/supervisor/jobs/__init__.py similarity index 100% rename from supervisor/job/__init__.py rename to supervisor/jobs/__init__.py diff --git a/supervisor/job/decorator.py b/supervisor/jobs/decorator.py similarity index 90% rename from supervisor/job/decorator.py rename to supervisor/jobs/decorator.py index 185898f4e..7909e40a7 100644 --- a/supervisor/job/decorator.py +++ b/supervisor/jobs/decorator.py @@ -53,7 +53,7 @@ class Job: job = self._coresys.jobs.get_job(self.name) - if self.conditions and not await self._check_conditions(): + if self.conditions and not self._check_conditions(): return False try: @@ -69,7 +69,7 @@ class Job: return wrapper - async def _check_conditions(self): + def _check_conditions(self): """Check conditions.""" if JobCondition.HEALTHY in self.conditions: if not self._coresys.core.healthy: @@ -93,10 +93,12 @@ class Job: return False if JobCondition.INTERNET in self.conditions: - if self._coresys.core.state == CoreState.RUNNING: - if self._coresys.dbus.network.is_connected: - await self._coresys.host.network.check_connectivity() - await self._coresys.supervisor.check_connectivity() + if self._coresys.core.state not in ( + CoreState.SETUP, + CoreState.RUNNING, + ): + return True + if not self._coresys.supervisor.connectivity: _LOGGER.warning( "'%s' blocked from execution, no supervisor internet connection", diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index 88461c3b5..8bf4a2120 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -12,7 +12,7 @@ from ..exceptions import ( MulticastError, ObserverError, ) -from ..job.decorator import Job, JobCondition +from ..jobs.decorator import Job, JobCondition _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -47,6 +47,8 @@ RUN_WATCHDOG_OBSERVER_APPLICATION = 180 RUN_REFRESH_ADDON = 15 +RUN_CHECK_CONNECTIVITY = 30 + class Tasks(CoreSysAttributes): """Handle Tasks inside Supervisor.""" @@ -111,6 +113,11 @@ class Tasks(CoreSysAttributes): # Refresh self.sys_scheduler.register_task(self._refresh_addon, RUN_REFRESH_ADDON) + # Connectivity + self.sys_scheduler.register_task( + self._check_connectivity, RUN_CHECK_CONNECTIVITY + ) + _LOGGER.info("All core tasks are scheduled") @Job(conditions=[JobCondition.HEALTHY, JobCondition.FREE_SPACE]) @@ -422,3 +429,29 @@ class Tasks(CoreSysAttributes): # Adjust state addon.state = AddonState.STOPPED + + async def _check_connectivity(self) -> None: + """Check system connectivity.""" + value = self._cache.get("connectivity", 0) + + # Need only full check if not connected or each 10min + if value >= 600: + pass + elif ( + self.sys_supervisor.connectivity + and self.sys_host.network.connectivity is None + ) or ( + self.sys_supervisor.connectivity + and self.sys_host.network.connectivity is not None + and self.sys_host.network.connectivity + ): + self._cache["connectivity"] = value + RUN_CHECK_CONNECTIVITY + return + + # Check connectivity + try: + await self.sys_supervisor.check_connectivity() + if self.sys_dbus.network.is_connected: + await self.sys_host.network.check_connectivity() + finally: + self._cache["connectivity"] = 0 diff --git a/supervisor/store/__init__.py b/supervisor/store/__init__.py index 956383280..2d6d14240 100644 --- a/supervisor/store/__init__.py +++ b/supervisor/store/__init__.py @@ -12,7 +12,7 @@ from supervisor.utils.json import read_json_file from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import JsonFileError, StoreError, StoreGitError -from ..job.decorator import Job, JobCondition +from ..jobs.decorator import Job, JobCondition from .addon import AddonStore from .data import StoreData from .repository import Repository diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index 4afffd5cd..b8488b089 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -33,7 +33,7 @@ class Supervisor(CoreSysAttributes): """Initialize hass object.""" self.coresys: CoreSys = coresys self.instance: DockerSupervisor = DockerSupervisor(coresys) - self._connectivity: bool = False + self._connectivity: bool = True async def load(self) -> None: """Prepare Home Assistant object.""" @@ -176,9 +176,10 @@ class Supervisor(CoreSysAttributes): async def check_connectivity(self): """Check the connection.""" + timeout = aiohttp.ClientTimeout(total=10) try: await self.sys_websession.head( - "https://version.home-assistant.io/online.txt", timeout=10 + "https://version.home-assistant.io/online.txt", timeout=timeout ) except (ClientError, asyncio.TimeoutError): self._connectivity = False diff --git a/supervisor/updater.py b/supervisor/updater.py index be14aebac..3a6295378 100644 --- a/supervisor/updater.py +++ b/supervisor/updater.py @@ -25,7 +25,7 @@ from .const import ( ) from .coresys import CoreSysAttributes from .exceptions import HassioUpdaterError -from .job.decorator import Job, JobCondition +from .jobs.decorator import Job, JobCondition from .utils import AsyncThrottle from .utils.json import JsonConfig from .validate import SCHEMA_UPDATER_CONFIG diff --git a/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.fixture b/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.fixture new file mode 100644 index 000000000..ea863053e --- /dev/null +++ b/tests/fixtures/org_freedesktop_NetworkManager-CheckConnectivity.fixture @@ -0,0 +1 @@ +[4] \ No newline at end of file diff --git a/tests/job/test_job_decorator.py b/tests/job/test_job_decorator.py index 2a298cd20..b1e20f964 100644 --- a/tests/job/test_job_decorator.py +++ b/tests/job/test_job_decorator.py @@ -2,8 +2,9 @@ # pylint: disable=protected-access,import-error from unittest.mock import patch +from supervisor.const import CoreState from supervisor.coresys import CoreSys -from supervisor.job.decorator import Job, JobCondition +from supervisor.jobs.decorator import Job, JobCondition async def test_healthy(coresys: CoreSys): @@ -30,6 +31,7 @@ async def test_healthy(coresys: CoreSys): async def test_internet(coresys: CoreSys): """Test the internet decorator.""" + coresys.core.state = CoreState.RUNNING class TestClass: """Test class.""" @@ -83,3 +85,44 @@ async def test_free_space(coresys: CoreSys): with patch("shutil.disk_usage", return_value=(42, 42, (512.0 ** 3))): assert not await test.execute() + + +async def test_internet_connectivity_with_core_state(coresys: CoreSys): + """Test the different core states and the impact for internet condition.""" + + class TestClass: + """Test class.""" + + def __init__(self, coresys: CoreSys): + """Initialize the test class.""" + self.coresys = coresys + + @Job(conditions=[JobCondition.INTERNET]) + async def execute(self): + """Execute the class method.""" + return True + + test = TestClass(coresys) + coresys.host.network._connectivity = False + coresys.supervisor._connectivity = False + + coresys.core.state = CoreState.INITIALIZE + assert await test.execute() + + coresys.core.state = CoreState.SETUP + assert not await test.execute() + + coresys.core.state = CoreState.STARTUP + assert await test.execute() + + coresys.core.state = CoreState.RUNNING + assert not await test.execute() + + coresys.core.state = CoreState.CLOSE + assert await test.execute() + + coresys.core.state = CoreState.SHUTDOWN + assert await test.execute() + + coresys.core.state = CoreState.STOPPING + assert await test.execute() diff --git a/tests/misc/test_connectivity_task.py b/tests/misc/test_connectivity_task.py new file mode 100644 index 000000000..01b79def6 --- /dev/null +++ b/tests/misc/test_connectivity_task.py @@ -0,0 +1,61 @@ +"""Test periodic connectivity task.""" +# pylint: disable=protected-access,import-error +from unittest.mock import AsyncMock + +from supervisor.coresys import CoreSys + + +async def test_no_connectivity(coresys: CoreSys): + """Test periodic connectivity task.""" + coresys.host.network.check_connectivity = AsyncMock() + coresys.supervisor.check_connectivity = AsyncMock() + + coresys.tasks._cache["connectivity"] = 0 + coresys.host.network._connectivity = False + coresys.supervisor._connectivity = False + + await coresys.tasks._check_connectivity() + + coresys.host.network.check_connectivity.assert_called_once() + coresys.supervisor.check_connectivity.assert_called_once() + assert coresys.tasks._cache["connectivity"] == 0 + coresys.host.network.check_connectivity.reset_mock() + coresys.supervisor.check_connectivity.reset_mock() + + await coresys.tasks._check_connectivity() + + coresys.host.network.check_connectivity.assert_called_once() + coresys.supervisor.check_connectivity.assert_called_once() + assert coresys.tasks._cache["connectivity"] == 0 + + +async def test_connectivity(coresys: CoreSys): + """Test periodic connectivity task.""" + coresys.host.network.check_connectivity = AsyncMock() + coresys.supervisor.check_connectivity = AsyncMock() + + coresys.tasks._cache["connectivity"] = 0 + coresys.host.network._connectivity = True + coresys.supervisor._connectivity = True + + await coresys.tasks._check_connectivity() + + coresys.host.network.check_connectivity.assert_not_called() + coresys.supervisor.check_connectivity.assert_not_called() + assert coresys.tasks._cache["connectivity"] == 30 + + +async def test_connectivity_cache_reached(coresys: CoreSys): + """Test periodic connectivity task.""" + coresys.host.network.check_connectivity = AsyncMock() + coresys.supervisor.check_connectivity = AsyncMock() + + coresys.tasks._cache["connectivity"] = 600 + coresys.host.network._connectivity = True + coresys.supervisor._connectivity = True + + await coresys.tasks._check_connectivity() + + coresys.host.network.check_connectivity.assert_called_once() + coresys.supervisor.check_connectivity.assert_called_once() + assert coresys.tasks._cache["connectivity"] == 0