Add resolution manager and unsupported flags (#2124)

* Add unsupported reason flags

* Restore test_network.py

* Add Resolution manager object

* fix import
This commit is contained in:
Joakim Sørensen 2020-10-13 12:54:17 +02:00 committed by GitHub
parent 8da686fc34
commit 028b170cff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 18 deletions

View File

@ -23,6 +23,7 @@ from .network import APINetwork
from .observer import APIObserver from .observer import APIObserver
from .os import APIOS from .os import APIOS
from .proxy import APIProxy from .proxy import APIProxy
from .resolution import APIResoulution
from .security import SecurityMiddleware from .security import SecurityMiddleware
from .services import APIServices from .services import APIServices
from .snapshots import APISnapshots from .snapshots import APISnapshots
@ -73,6 +74,7 @@ class RestAPI(CoreSysAttributes):
self._register_os() self._register_os()
self._register_panel() self._register_panel()
self._register_proxy() self._register_proxy()
self._register_resolution()
self._register_services() self._register_services()
self._register_snapshots() self._register_snapshots()
self._register_supervisor() self._register_supervisor()
@ -190,6 +192,13 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes([web.get("/info", api_info.info)]) 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: def _register_auth(self) -> None:
"""Register auth functions.""" """Register auth functions."""
api_auth = APIAuth() api_auth = APIAuth()

View File

@ -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}

View File

@ -39,6 +39,7 @@ from .misc.hwmon import HwMonitor
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
from .misc.tasks import Tasks from .misc.tasks import Tasks
from .plugins import PluginManager from .plugins import PluginManager
from .resolution import ResolutionManager
from .services import ServiceManager from .services import ServiceManager
from .snapshots import SnapshotManager from .snapshots import SnapshotManager
from .store import StoreManager from .store import StoreManager
@ -54,6 +55,7 @@ async def initialize_coresys() -> CoreSys:
coresys = CoreSys() coresys = CoreSys()
# Initialize core objects # Initialize core objects
coresys.resolution = ResolutionManager(coresys)
coresys.core = Core(coresys) coresys.core = Core(coresys)
coresys.plugins = PluginManager(coresys) coresys.plugins = PluginManager(coresys)
coresys.arch = CpuArch(coresys) coresys.arch = CpuArch(coresys)

View File

@ -261,6 +261,7 @@ ATTR_TOTP = "totp"
ATTR_TYPE = "type" ATTR_TYPE = "type"
ATTR_UDEV = "udev" ATTR_UDEV = "udev"
ATTR_UNSAVED = "unsaved" ATTR_UNSAVED = "unsaved"
ATTR_UNSUPPORTED = "unsupported"
ATTR_URL = "url" ATTR_URL = "url"
ATTR_USB = "usb" ATTR_USB = "usb"
ATTR_USER = "user" ATTR_USER = "user"
@ -428,3 +429,17 @@ class HostFeature(str, Enum):
REBOOT = "reboot" REBOOT = "reboot"
SERVICES = "services" SERVICES = "services"
SHUTDOWN = "shutdown" 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"

View File

@ -13,6 +13,7 @@ from .const import (
AddonStartup, AddonStartup,
CoreState, CoreState,
HostFeature, HostFeature,
UnsupportedReason,
) )
from .coresys import CoreSys, CoreSysAttributes from .coresys import CoreSys, CoreSysAttributes
from .exceptions import ( from .exceptions import (
@ -33,8 +34,6 @@ class Core(CoreSysAttributes):
"""Initialize Supervisor object.""" """Initialize Supervisor object."""
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.healthy: bool = True self.healthy: bool = True
self.supported: bool = True
self._state: Optional[CoreState] = None self._state: Optional[CoreState] = None
@property @property
@ -42,6 +41,11 @@ class Core(CoreSysAttributes):
"""Return state of the core.""" """Return state of the core."""
return self._state return self._state
@property
def supported(self) -> CoreState:
"""Return true if the installation is supported."""
return len(self.sys_resolution.unsupported) == 0
@state.setter @state.setter
def state(self, new_state: CoreState) -> None: def state(self, new_state: CoreState) -> None:
"""Set core into new state.""" """Set core into new state."""
@ -61,29 +65,29 @@ class Core(CoreSysAttributes):
# If host docker is supported? # If host docker is supported?
if not self.sys_docker.info.supported_version: if not self.sys_docker.info.supported_version:
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.DOCKER_VERSION
self.healthy = False self.healthy = False
_LOGGER.error( _LOGGER.error(
"Docker version %s is not supported by Supervisor!", "Docker version %s is not supported by Supervisor!",
self.sys_docker.info.version, self.sys_docker.info.version,
) )
elif self.sys_docker.info.inside_lxc: elif self.sys_docker.info.inside_lxc:
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.LXC
self.healthy = False self.healthy = False
_LOGGER.error( _LOGGER.error(
"Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!" "Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!"
) )
elif not self.sys_supervisor.instance.privileged: elif not self.sys_supervisor.instance.privileged:
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.PRIVILEGED
self.healthy = False self.healthy = False
_LOGGER.error("Supervisor does not run in Privileged mode.") _LOGGER.error("Supervisor does not run in Privileged mode.")
if self.sys_docker.info.check_requirements(): if self.sys_docker.info.check_requirements():
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.DOCKER_CONFIGURATION
# Dbus available # Dbus available
if not SOCKET_DBUS.exists(): if not SOCKET_DBUS.exists():
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.DBUS
_LOGGER.error( _LOGGER.error(
"DBus is required for Home Assistant. This system is not supported!" "DBus is required for Home Assistant. This system is not supported!"
) )
@ -158,7 +162,7 @@ class Core(CoreSysAttributes):
# Check supported OS # Check supported OS
if not self.sys_hassos.available: if not self.sys_hassos.available:
if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS: if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS:
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.OS
_LOGGER.error( _LOGGER.error(
"Detected unsupported OS: %s", "Detected unsupported OS: %s",
self.sys_host.info.operating_system, self.sys_host.info.operating_system,
@ -166,7 +170,7 @@ class Core(CoreSysAttributes):
# Check Host features # Check Host features
if HostFeature.NETWORK not in self.sys_host.supported_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") _LOGGER.error("NetworkManager is not correct working")
if any( if any(
feature not in self.sys_host.supported_features feature not in self.sys_host.supported_features
@ -177,13 +181,13 @@ class Core(CoreSysAttributes):
HostFeature.REBOOT, HostFeature.REBOOT,
) )
): ):
self.supported = False self.sys_resolution.unsupported = UnsupportedReason.SYSTEMD
_LOGGER.error("Systemd is not correct working") _LOGGER.error("Systemd is not correct working")
# Check if image names from denylist exist # Check if image names from denylist exist
try: try:
if await self.sys_run_in_executor(self.sys_docker.check_denylist_images): 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 self.healthy = False
except DockerError: except DockerError:
self.healthy = False self.healthy = False

View File

@ -33,6 +33,7 @@ if TYPE_CHECKING:
from .store import StoreManager from .store import StoreManager
from .updater import Updater from .updater import Updater
from .plugins import PluginManager from .plugins import PluginManager
from .resolution import ResolutionManager
T = TypeVar("T") T = TypeVar("T")
@ -80,6 +81,7 @@ class CoreSys:
self._discovery: Optional[Discovery] = None self._discovery: Optional[Discovery] = None
self._hwmonitor: Optional[HwMonitor] = None self._hwmonitor: Optional[HwMonitor] = None
self._plugins: Optional[PluginManager] = None self._plugins: Optional[PluginManager] = None
self._resolution: Optional[ResolutionManager] = None
@property @property
def dev(self) -> bool: def dev(self) -> bool:
@ -398,6 +400,20 @@ class CoreSys:
raise RuntimeError("HassOS already set!") raise RuntimeError("HassOS already set!")
self._hassos = value 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 @property
def machine(self) -> Optional[str]: def machine(self) -> Optional[str]:
"""Return machine type string.""" """Return machine type string."""
@ -568,6 +584,11 @@ class CoreSysAttributes:
"""Return HassOS object.""" """Return HassOS object."""
return self.coresys.hassos return self.coresys.hassos
@property
def sys_resolution(self) -> ResolutionManager:
"""Return Resolution manager object."""
return self.coresys.resolution
def sys_run_in_executor( def sys_run_in_executor(
self, funct: Callable[..., T], *args: Any self, funct: Callable[..., T], *args: Any
) -> Coroutine[Any, Any, T]: ) -> Coroutine[Any, Any, T]:

View File

@ -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)

View File

@ -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]

View File

@ -4,7 +4,7 @@ from unittest.mock import patch
import pytest import pytest
from supervisor.const import SUPERVISOR_VERSION, CoreState from supervisor.const import SUPERVISOR_VERSION, CoreState, UnsupportedReason
from supervisor.exceptions import AddonConfigurationError from supervisor.exceptions import AddonConfigurationError
from supervisor.misc.filter import filter_data from supervisor.misc.filter import filter_data
@ -27,21 +27,19 @@ def test_ignored_exception(coresys):
def test_diagnostics_disabled(coresys): def test_diagnostics_disabled(coresys):
"""Test if diagnostics is disabled.""" """Test if diagnostics is disabled."""
coresys.config.diagnostics = False coresys.config.diagnostics = False
coresys.core.supported = True
assert filter_data(coresys, SAMPLE_EVENT, {}) is None assert filter_data(coresys, SAMPLE_EVENT, {}) is None
def test_not_supported(coresys): def test_not_supported(coresys):
"""Test if not supported.""" """Test if not supported."""
coresys.config.diagnostics = True coresys.config.diagnostics = True
coresys.core.supported = False coresys.resolution.unsupported = UnsupportedReason.DOCKER_VERSION
assert filter_data(coresys, SAMPLE_EVENT, {}) is None assert filter_data(coresys, SAMPLE_EVENT, {}) is None
def test_is_dev(coresys): def test_is_dev(coresys):
"""Test if dev.""" """Test if dev."""
coresys.config.diagnostics = True coresys.config.diagnostics = True
coresys.core.supported = True
with patch("os.environ", return_value=[("ENV_SUPERVISOR_DEV", "1")]): with patch("os.environ", return_value=[("ENV_SUPERVISOR_DEV", "1")]):
assert filter_data(coresys, SAMPLE_EVENT, {}) is None assert filter_data(coresys, SAMPLE_EVENT, {}) is None
@ -49,7 +47,6 @@ def test_is_dev(coresys):
def test_not_started(coresys): def test_not_started(coresys):
"""Test if supervisor not fully started.""" """Test if supervisor not fully started."""
coresys.config.diagnostics = True coresys.config.diagnostics = True
coresys.core.supported = True
coresys.core.state = CoreState.INITIALIZE coresys.core.state = CoreState.INITIALIZE
assert filter_data(coresys, SAMPLE_EVENT, {}) == SAMPLE_EVENT assert filter_data(coresys, SAMPLE_EVENT, {}) == SAMPLE_EVENT
@ -61,7 +58,6 @@ def test_not_started(coresys):
def test_defaults(coresys): def test_defaults(coresys):
"""Test event defaults.""" """Test event defaults."""
coresys.config.diagnostics = True coresys.config.diagnostics = True
coresys.supported = True
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): 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.config.diagnostics = True
coresys.supported = True
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))):

View File

@ -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