From 028b170cffca5f3a830911a9bfba8da730080583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 13 Oct 2020 12:54:17 +0200 Subject: [PATCH] Add resolution manager and unsupported flags (#2124) * Add unsupported reason flags * Restore test_network.py * Add Resolution manager object * fix import --- supervisor/api/__init__.py | 9 +++++++ supervisor/api/resolution.py | 17 ++++++++++++++ supervisor/bootstrap.py | 2 ++ supervisor/const.py | 15 ++++++++++++ supervisor/core.py | 26 ++++++++++++--------- supervisor/coresys.py | 21 +++++++++++++++++ supervisor/resolution/__init__.py | 24 +++++++++++++++++++ tests/api/test_resolution.py | 13 +++++++++++ tests/misc/test_filter_data.py | 9 ++----- tests/resolution/test_resolution_manager.py | 14 +++++++++++ 10 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 supervisor/api/resolution.py create mode 100644 supervisor/resolution/__init__.py create mode 100644 tests/api/test_resolution.py create mode 100644 tests/resolution/test_resolution_manager.py diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 08bdcdfd8..3a492b513 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -23,6 +23,7 @@ from .network import APINetwork from .observer import APIObserver from .os import APIOS from .proxy import APIProxy +from .resolution import APIResoulution from .security import SecurityMiddleware from .services import APIServices from .snapshots import APISnapshots @@ -73,6 +74,7 @@ class RestAPI(CoreSysAttributes): self._register_os() self._register_panel() self._register_proxy() + self._register_resolution() self._register_services() self._register_snapshots() self._register_supervisor() @@ -190,6 +192,13 @@ class RestAPI(CoreSysAttributes): self.webapp.add_routes([web.get("/info", api_info.info)]) + def _register_resolution(self) -> None: + """Register info functions.""" + api_resolution = APIResoulution() + api_resolution.coresys = self.coresys + + self.webapp.add_routes([web.get("/resolution", api_resolution.base)]) + def _register_auth(self) -> None: """Register auth functions.""" api_auth = APIAuth() diff --git a/supervisor/api/resolution.py b/supervisor/api/resolution.py new file mode 100644 index 000000000..391f051f3 --- /dev/null +++ b/supervisor/api/resolution.py @@ -0,0 +1,17 @@ +"""Handle REST API for resoulution.""" +from typing import Any, Dict + +from aiohttp import web + +from ..const import ATTR_UNSUPPORTED +from ..coresys import CoreSysAttributes +from .utils import api_process + + +class APIResoulution(CoreSysAttributes): + """Handle REST API for resoulution.""" + + @api_process + async def base(self, request: web.Request) -> Dict[str, Any]: + """Return network information.""" + return {ATTR_UNSUPPORTED: self.sys_resolution.unsupported} diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 9232f0acf..4ca15b91e 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -39,6 +39,7 @@ from .misc.hwmon import HwMonitor from .misc.scheduler import Scheduler from .misc.tasks import Tasks from .plugins import PluginManager +from .resolution import ResolutionManager from .services import ServiceManager from .snapshots import SnapshotManager from .store import StoreManager @@ -54,6 +55,7 @@ async def initialize_coresys() -> CoreSys: coresys = CoreSys() # Initialize core objects + coresys.resolution = ResolutionManager(coresys) coresys.core = Core(coresys) coresys.plugins = PluginManager(coresys) coresys.arch = CpuArch(coresys) diff --git a/supervisor/const.py b/supervisor/const.py index 70a03cabc..9242ccf33 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -261,6 +261,7 @@ ATTR_TOTP = "totp" ATTR_TYPE = "type" ATTR_UDEV = "udev" ATTR_UNSAVED = "unsaved" +ATTR_UNSUPPORTED = "unsupported" ATTR_URL = "url" ATTR_USB = "usb" ATTR_USER = "user" @@ -428,3 +429,17 @@ class HostFeature(str, Enum): REBOOT = "reboot" SERVICES = "services" SHUTDOWN = "shutdown" + + +class UnsupportedReason(str, Enum): + """Reasons for unsupported status.""" + + CONTAINER = "container" + DBUS = "dbus" + DOCKER_CONFIGURATION = "docker_configuration" + DOCKER_VERSION = "docker_version" + LXC = "lxc" + NETWORK_MANAGER = "network_manager" + OS = "os" + PRIVILEGED = "privileged" + SYSTEMD = "systemd" diff --git a/supervisor/core.py b/supervisor/core.py index e4dfae37b..c81b32269 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -13,6 +13,7 @@ from .const import ( AddonStartup, CoreState, HostFeature, + UnsupportedReason, ) from .coresys import CoreSys, CoreSysAttributes from .exceptions import ( @@ -33,8 +34,6 @@ class Core(CoreSysAttributes): """Initialize Supervisor object.""" self.coresys: CoreSys = coresys self.healthy: bool = True - self.supported: bool = True - self._state: Optional[CoreState] = None @property @@ -42,6 +41,11 @@ class Core(CoreSysAttributes): """Return state of the core.""" return self._state + @property + def supported(self) -> CoreState: + """Return true if the installation is supported.""" + return len(self.sys_resolution.unsupported) == 0 + @state.setter def state(self, new_state: CoreState) -> None: """Set core into new state.""" @@ -61,29 +65,29 @@ class Core(CoreSysAttributes): # If host docker is supported? if not self.sys_docker.info.supported_version: - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.DOCKER_VERSION self.healthy = False _LOGGER.error( "Docker version %s is not supported by Supervisor!", self.sys_docker.info.version, ) elif self.sys_docker.info.inside_lxc: - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.LXC self.healthy = False _LOGGER.error( "Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!" ) elif not self.sys_supervisor.instance.privileged: - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.PRIVILEGED self.healthy = False _LOGGER.error("Supervisor does not run in Privileged mode.") if self.sys_docker.info.check_requirements(): - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.DOCKER_CONFIGURATION # Dbus available if not SOCKET_DBUS.exists(): - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.DBUS _LOGGER.error( "DBus is required for Home Assistant. This system is not supported!" ) @@ -158,7 +162,7 @@ class Core(CoreSysAttributes): # Check supported OS if not self.sys_hassos.available: if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS: - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.OS _LOGGER.error( "Detected unsupported OS: %s", self.sys_host.info.operating_system, @@ -166,7 +170,7 @@ class Core(CoreSysAttributes): # Check Host features if HostFeature.NETWORK not in self.sys_host.supported_features: - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.NETWORK_MANAGER _LOGGER.error("NetworkManager is not correct working") if any( feature not in self.sys_host.supported_features @@ -177,13 +181,13 @@ class Core(CoreSysAttributes): HostFeature.REBOOT, ) ): - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.SYSTEMD _LOGGER.error("Systemd is not correct working") # Check if image names from denylist exist try: if await self.sys_run_in_executor(self.sys_docker.check_denylist_images): - self.supported = False + self.sys_resolution.unsupported = UnsupportedReason.CONTAINER self.healthy = False except DockerError: self.healthy = False diff --git a/supervisor/coresys.py b/supervisor/coresys.py index e98d308ef..027e0f70c 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from .store import StoreManager from .updater import Updater from .plugins import PluginManager + from .resolution import ResolutionManager T = TypeVar("T") @@ -80,6 +81,7 @@ class CoreSys: self._discovery: Optional[Discovery] = None self._hwmonitor: Optional[HwMonitor] = None self._plugins: Optional[PluginManager] = None + self._resolution: Optional[ResolutionManager] = None @property def dev(self) -> bool: @@ -398,6 +400,20 @@ class CoreSys: raise RuntimeError("HassOS already set!") self._hassos = value + @property + def resolution(self) -> ResolutionManager: + """Return resolution manager object.""" + if self._resolution is None: + raise RuntimeError("resolution manager not set!") + return self._resolution + + @resolution.setter + def resolution(self, value: ResolutionManager) -> None: + """Set a resolution manager object.""" + if self._resolution: + raise RuntimeError("resolution manager already set!") + self._resolution = value + @property def machine(self) -> Optional[str]: """Return machine type string.""" @@ -568,6 +584,11 @@ class CoreSysAttributes: """Return HassOS object.""" return self.coresys.hassos + @property + def sys_resolution(self) -> ResolutionManager: + """Return Resolution manager object.""" + return self.coresys.resolution + def sys_run_in_executor( self, funct: Callable[..., T], *args: Any ) -> Coroutine[Any, Any, T]: diff --git a/supervisor/resolution/__init__.py b/supervisor/resolution/__init__.py new file mode 100644 index 000000000..68eb4009b --- /dev/null +++ b/supervisor/resolution/__init__.py @@ -0,0 +1,24 @@ +"""Supervisor resolution center.""" +from typing import List + +from ..const import UnsupportedReason +from ..coresys import CoreSys, CoreSysAttributes + + +class ResolutionManager(CoreSysAttributes): + """Resolution manager for supervisor.""" + + def __init__(self, coresys: CoreSys): + """Initialize Resolution manager.""" + self.coresys: CoreSys = coresys + self._unsupported: List[UnsupportedReason] = [] + + @property + def unsupported(self) -> List[UnsupportedReason]: + """Return a list of unsupported reasons.""" + return self._unsupported + + @unsupported.setter + def unsupported(self, reason: UnsupportedReason) -> None: + """Add a reason for unsupported.""" + self._unsupported.append(reason) diff --git a/tests/api/test_resolution.py b/tests/api/test_resolution.py new file mode 100644 index 000000000..756291c55 --- /dev/null +++ b/tests/api/test_resolution.py @@ -0,0 +1,13 @@ +"""Test Resolution API.""" +import pytest + +from supervisor.const import ATTR_UNSUPPORTED, UnsupportedReason + + +@pytest.mark.asyncio +async def test_api_resolution_base(coresys, api_client): + """Test resolution manager api.""" + coresys.resolution.unsupported = UnsupportedReason.OS + resp = await api_client.get("/resolution") + result = await resp.json() + assert UnsupportedReason.OS in result["data"][ATTR_UNSUPPORTED] diff --git a/tests/misc/test_filter_data.py b/tests/misc/test_filter_data.py index eb6dcfcf7..02109abd0 100644 --- a/tests/misc/test_filter_data.py +++ b/tests/misc/test_filter_data.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from supervisor.const import SUPERVISOR_VERSION, CoreState +from supervisor.const import SUPERVISOR_VERSION, CoreState, UnsupportedReason from supervisor.exceptions import AddonConfigurationError from supervisor.misc.filter import filter_data @@ -27,21 +27,19 @@ def test_ignored_exception(coresys): def test_diagnostics_disabled(coresys): """Test if diagnostics is disabled.""" coresys.config.diagnostics = False - coresys.core.supported = True assert filter_data(coresys, SAMPLE_EVENT, {}) is None def test_not_supported(coresys): """Test if not supported.""" coresys.config.diagnostics = True - coresys.core.supported = False + coresys.resolution.unsupported = UnsupportedReason.DOCKER_VERSION assert filter_data(coresys, SAMPLE_EVENT, {}) is None def test_is_dev(coresys): """Test if dev.""" coresys.config.diagnostics = True - coresys.core.supported = True with patch("os.environ", return_value=[("ENV_SUPERVISOR_DEV", "1")]): assert filter_data(coresys, SAMPLE_EVENT, {}) is None @@ -49,7 +47,6 @@ def test_is_dev(coresys): def test_not_started(coresys): """Test if supervisor not fully started.""" coresys.config.diagnostics = True - coresys.core.supported = True coresys.core.state = CoreState.INITIALIZE assert filter_data(coresys, SAMPLE_EVENT, {}) == SAMPLE_EVENT @@ -61,7 +58,6 @@ def test_not_started(coresys): def test_defaults(coresys): """Test event defaults.""" coresys.config.diagnostics = True - coresys.supported = True coresys.core.state = CoreState.RUNNING with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): @@ -89,7 +85,6 @@ def test_sanitize(coresys): }, } coresys.config.diagnostics = True - coresys.supported = True coresys.core.state = CoreState.RUNNING with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): diff --git a/tests/resolution/test_resolution_manager.py b/tests/resolution/test_resolution_manager.py new file mode 100644 index 000000000..7c7684d31 --- /dev/null +++ b/tests/resolution/test_resolution_manager.py @@ -0,0 +1,14 @@ +"""Tests for resolution manager.""" + + +from supervisor.const import UnsupportedReason +from supervisor.coresys import CoreSys + + +def test_properies(coresys: CoreSys): + """Test resolution manager properties.""" + + assert coresys.core.supported + + coresys.resolution.unsupported = UnsupportedReason.OS + assert not coresys.core.supported