Handle Unhealthy like Unsupported (#2255)

* Handle Unhealthy like Unsupported

* Add tests

* Add unhealthy to sentry

* Add test
This commit is contained in:
Pascal Vizeli 2020-11-14 16:16:00 +01:00 committed by GitHub
parent 7ee5737f75
commit 2040102e21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 101 additions and 16 deletions

View File

@ -18,6 +18,7 @@ from ..exceptions import (
HomeAssistantAPIError, HomeAssistantAPIError,
HostAppArmorError, HostAppArmorError,
) )
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..store.addon import AddonStore from ..store.addon import AddonStore
from ..utils import check_exception_chain from ..utils import check_exception_chain
from .addon import Addon from .addon import Addon
@ -376,7 +377,12 @@ class AddonManager(CoreSysAttributes):
continue continue
except DockerError as err: except DockerError as err:
_LOGGER.warning("Add-on %s is corrupt: %s", addon.slug, err) _LOGGER.warning("Add-on %s is corrupt: %s", addon.slug, err)
self.sys_core.healthy = False self.sys_resolution.create_issue(
IssueType.CORRUPT_DOCKER,
ContextType.ADDON,
reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR],
)
self.sys_capture_exception(err) self.sys_capture_exception(err)
else: else:
self.sys_plugins.dns.add_host( self.sys_plugins.dns.add_host(

View File

@ -4,7 +4,7 @@ from typing import Any, Dict
from aiohttp import web from aiohttp import web
import attr import attr
from ..const import ATTR_ISSUES, ATTR_SUGGESTIONS, ATTR_UNSUPPORTED from ..const import ATTR_ISSUES, ATTR_SUGGESTIONS, ATTR_UNHEALTHY, ATTR_UNSUPPORTED
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, ResolutionNotFound from ..exceptions import APIError, ResolutionNotFound
from .utils import api_process from .utils import api_process
@ -18,6 +18,7 @@ class APIResoulution(CoreSysAttributes):
"""Return network information.""" """Return network information."""
return { return {
ATTR_UNSUPPORTED: self.sys_resolution.unsupported, ATTR_UNSUPPORTED: self.sys_resolution.unsupported,
ATTR_UNHEALTHY: self.sys_resolution.unhealthy,
ATTR_SUGGESTIONS: [ ATTR_SUGGESTIONS: [
attr.asdict(suggestion) attr.asdict(suggestion)
for suggestion in self.sys_resolution.suggestions for suggestion in self.sys_resolution.suggestions

View File

@ -289,6 +289,7 @@ ATTR_SIGNAL = "signal"
ATTR_MAC = "mac" ATTR_MAC = "mac"
ATTR_FREQUENCY = "frequency" ATTR_FREQUENCY = "frequency"
ATTR_ACCESSPOINTS = "accesspoints" ATTR_ACCESSPOINTS = "accesspoints"
ATTR_UNHEALTHY = "unhealthy"
PROVIDE_SERVICE = "provide" PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need" NEED_SERVICE = "need"

View File

@ -14,7 +14,7 @@ from .exceptions import (
HomeAssistantError, HomeAssistantError,
SupervisorUpdateError, SupervisorUpdateError,
) )
from .resolution.const import ContextType, IssueType from .resolution.const import ContextType, IssueType, UnhealthyReason
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -25,7 +25,6 @@ class Core(CoreSysAttributes):
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize Supervisor object.""" """Initialize Supervisor object."""
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.healthy: bool = True
self._state: Optional[CoreState] = None self._state: Optional[CoreState] = None
@property @property
@ -34,10 +33,15 @@ class Core(CoreSysAttributes):
return self._state return self._state
@property @property
def supported(self) -> CoreState: def supported(self) -> bool:
"""Return true if the installation is supported.""" """Return true if the installation is supported."""
return len(self.sys_resolution.unsupported) == 0 return len(self.sys_resolution.unsupported) == 0
@property
def healthy(self) -> bool:
"""Return true if the installation is healthy."""
return len(self.sys_resolution.unhealthy) == 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."""
@ -67,7 +71,7 @@ class Core(CoreSysAttributes):
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.UPDATE_ROLLBACK, ContextType.SUPERVISOR IssueType.UPDATE_ROLLBACK, ContextType.SUPERVISOR
) )
self.healthy = False self.sys_resolution.unhealthy = UnhealthyReason.SUPERVISOR
_LOGGER.error( _LOGGER.error(
"Update '%s' of Supervisor '%s' failed!", "Update '%s' of Supervisor '%s' failed!",
self.sys_config.version, self.sys_config.version,
@ -119,7 +123,7 @@ class Core(CoreSysAttributes):
_LOGGER.critical( _LOGGER.critical(
"Fatal error happening on load Task %s: %s", setup_task, err "Fatal error happening on load Task %s: %s", setup_task, err
) )
self.healthy = False self.sys_resolution.unhealthy = UnhealthyReason.SETUP
self.sys_capture_exception(err) self.sys_capture_exception(err)
# Evaluate the system # Evaluate the system

View File

@ -75,6 +75,7 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
"supervisor": coresys.supervisor.version, "supervisor": coresys.supervisor.version,
}, },
"issues": [attr.asdict(issue) for issue in coresys.resolution.issues], "issues": [attr.asdict(issue) for issue in coresys.resolution.issues],
"unhealthy": coresys.resolution.unhealthy,
} }
) )
event.setdefault("tags", []).extend( event.setdefault("tags", []).extend(

View File

@ -7,6 +7,7 @@ from typing import Optional
import pyudev import pyudev
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..resolution.const import UnhealthyReason
from ..utils import AsyncCallFilter from ..utils import AsyncCallFilter
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -28,7 +29,7 @@ class HwMonitor(CoreSysAttributes):
self.monitor = pyudev.Monitor.from_netlink(self.context) self.monitor = pyudev.Monitor.from_netlink(self.context)
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events) self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
except OSError: except OSError:
self.sys_core.healthy = False self.sys_resolution.unhealthy = UnhealthyReason.PRIVILEGED
_LOGGER.critical("Not privileged to run udev monitor!") _LOGGER.critical("Not privileged to run udev monitor!")
else: else:
self.observer.start() self.observer.start()

View File

@ -9,6 +9,7 @@ from .const import (
ContextType, ContextType,
IssueType, IssueType,
SuggestionType, SuggestionType,
UnhealthyReason,
UnsupportedReason, UnsupportedReason,
) )
from .data import Issue, Suggestion from .data import Issue, Suggestion
@ -32,6 +33,7 @@ class ResolutionManager(CoreSysAttributes):
self._suggestions: List[Suggestion] = [] self._suggestions: List[Suggestion] = []
self._issues: List[Issue] = [] self._issues: List[Issue] = []
self._unsupported: List[UnsupportedReason] = [] self._unsupported: List[UnsupportedReason] = []
self._unhealthy: List[UnhealthyReason] = []
@property @property
def evaluate(self) -> ResolutionEvaluation: def evaluate(self) -> ResolutionEvaluation:
@ -81,6 +83,17 @@ class ResolutionManager(CoreSysAttributes):
if reason not in self._unsupported: if reason not in self._unsupported:
self._unsupported.append(reason) self._unsupported.append(reason)
@property
def unhealthy(self) -> List[UnhealthyReason]:
"""Return a list of unsupported reasons."""
return self._unhealthy
@unhealthy.setter
def unhealthy(self, reason: UnhealthyReason) -> None:
"""Add a reason for unsupported."""
if reason not in self._unhealthy:
self._unhealthy.append(reason)
def get_suggestion(self, uuid: str) -> Suggestion: def get_suggestion(self, uuid: str) -> Suggestion:
"""Return suggestion with uuid.""" """Return suggestion with uuid."""
for suggestion in self._suggestions: for suggestion in self._suggestions:

View File

@ -33,6 +33,15 @@ class UnsupportedReason(str, Enum):
SYSTEMD = "systemd" SYSTEMD = "systemd"
class UnhealthyReason(str, Enum):
"""Reasons for unsupported status."""
DOCKER = "docker"
SUPERVISOR = "supervisor"
SETUP = "setup"
PRIVILEGED = "privileged"
class IssueType(str, Enum): class IssueType(str, Enum):
"""Issue type.""" """Issue type."""

View File

@ -2,7 +2,7 @@
import logging import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from .const import UnsupportedReason from .const import UnhealthyReason, UnsupportedReason
from .evaluations.container import EvaluateContainer from .evaluations.container import EvaluateContainer
from .evaluations.dbus import EvaluateDbus from .evaluations.dbus import EvaluateDbus
from .evaluations.docker_configuration import EvaluateDockerConfiguration from .evaluations.docker_configuration import EvaluateDockerConfiguration
@ -54,6 +54,6 @@ class ResolutionEvaluation(CoreSysAttributes):
await self._systemd() await self._systemd()
if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY): if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY):
self.sys_core.healthy = False self.sys_resolution.unhealthy = UnhealthyReason.DOCKER
_LOGGER.info("System evaluation complete") _LOGGER.info("System evaluation complete")

View File

@ -16,6 +16,7 @@ from ..const import (
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import JsonFileError from ..exceptions import JsonFileError
from ..resolution.const import ContextType, IssueType
from ..utils.json import read_json_file from ..utils.json import read_json_file
from .utils import extract_hash_from_path from .utils import extract_hash_from_path
from .validate import SCHEMA_REPOSITORY_CONFIG from .validate import SCHEMA_REPOSITORY_CONFIG
@ -82,7 +83,9 @@ class StoreData(CoreSysAttributes):
if ".git" not in addon.parts if ".git" not in addon.parts
] ]
except OSError as err: except OSError as err:
self.sys_core.healthy = False self.sys_resolution.create_issue(
IssueType.CORRUPT_REPOSITORY, ContextType.SYSTEM
)
_LOGGER.critical( _LOGGER.critical(
"Can't process %s because of Filesystem issues: %s", repository, err "Can't process %s because of Filesystem issues: %s", repository, err
) )

View File

@ -3,13 +3,19 @@ from unittest.mock import AsyncMock
import pytest import pytest
from supervisor.const import ATTR_ISSUES, ATTR_SUGGESTIONS, ATTR_UNSUPPORTED from supervisor.const import (
ATTR_ISSUES,
ATTR_SUGGESTIONS,
ATTR_UNHEALTHY,
ATTR_UNSUPPORTED,
)
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import ResolutionError from supervisor.exceptions import ResolutionError
from supervisor.resolution.const import ( from supervisor.resolution.const import (
ContextType, ContextType,
IssueType, IssueType,
SuggestionType, SuggestionType,
UnhealthyReason,
UnsupportedReason, UnsupportedReason,
) )
from supervisor.resolution.data import Issue, Suggestion from supervisor.resolution.data import Issue, Suggestion
@ -84,3 +90,13 @@ async def test_api_resolution_dismiss_issue(coresys: CoreSys, api_client):
assert IssueType.UPDATE_FAILED == coresys.resolution.issues[-1].type assert IssueType.UPDATE_FAILED == coresys.resolution.issues[-1].type
await api_client.delete(f"/resolution/issue/{updated_failed.uuid}") await api_client.delete(f"/resolution/issue/{updated_failed.uuid}")
assert updated_failed not in coresys.resolution.issues assert updated_failed not in coresys.resolution.issues
@pytest.mark.asyncio
async def test_api_resolution_unhealthy(coresys: CoreSys, api_client):
"""Test resolution manager api."""
coresys.resolution.unhealthy = UnhealthyReason.DOCKER
resp = await api_client.get("/resolution/info")
result = await resp.json()
assert UnhealthyReason.DOCKER == result["data"][ATTR_UNHEALTHY][-1]

View File

@ -5,6 +5,7 @@ from unittest.mock import patch
from supervisor.const import CoreState from supervisor.const import CoreState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.jobs.decorator import Job, JobCondition from supervisor.jobs.decorator import Job, JobCondition
from supervisor.resolution.const import UnhealthyReason
async def test_healthy(coresys: CoreSys): async def test_healthy(coresys: CoreSys):
@ -25,7 +26,7 @@ async def test_healthy(coresys: CoreSys):
test = TestClass(coresys) test = TestClass(coresys)
assert await test.execute() assert await test.execute()
coresys.core.healthy = False coresys.resolution.unhealthy = UnhealthyReason.DOCKER
assert not await test.execute() assert not await test.execute()

View File

@ -7,7 +7,12 @@ import pytest
from supervisor.const import SUPERVISOR_VERSION, CoreState from supervisor.const import SUPERVISOR_VERSION, CoreState
from supervisor.exceptions import AddonConfigurationError from supervisor.exceptions import AddonConfigurationError
from supervisor.misc.filter import filter_data from supervisor.misc.filter import filter_data
from supervisor.resolution.const import ContextType, IssueType, UnsupportedReason from supervisor.resolution.const import (
ContextType,
IssueType,
UnhealthyReason,
UnsupportedReason,
)
SAMPLE_EVENT = {"sample": "event", "extra": {"Test": "123"}} SAMPLE_EVENT = {"sample": "event", "extra": {"Test": "123"}}
@ -117,3 +122,17 @@ def test_issues_on_report(coresys):
assert "issues" in event["contexts"] assert "issues" in event["contexts"]
assert event["contexts"]["issues"][0]["type"] == IssueType.FATAL_ERROR assert event["contexts"]["issues"][0]["type"] == IssueType.FATAL_ERROR
assert event["contexts"]["issues"][0]["context"] == ContextType.SYSTEM assert event["contexts"]["issues"][0]["context"] == ContextType.SYSTEM
def test_unhealthy_on_report(coresys):
"""Attach unhealthy to report."""
coresys.config.diagnostics = True
coresys.core.state = CoreState.RUNNING
coresys.resolution.unhealthy = UnhealthyReason.DOCKER
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))):
event = filter_data(coresys, SAMPLE_EVENT, {})
assert "issues" in event["contexts"]
assert event["contexts"]["unhealthy"][-1] == UnhealthyReason.DOCKER

View File

@ -17,6 +17,7 @@ from supervisor.resolution.const import (
ContextType, ContextType,
IssueType, IssueType,
SuggestionType, SuggestionType,
UnhealthyReason,
UnsupportedReason, UnsupportedReason,
) )
from supervisor.resolution.data import Issue, Suggestion from supervisor.resolution.data import Issue, Suggestion
@ -25,8 +26,8 @@ from supervisor.utils.dt import utcnow
from supervisor.utils.tar import SecureTarFile from supervisor.utils.tar import SecureTarFile
def test_properies(coresys: CoreSys): def test_properies_unsupported(coresys: CoreSys):
"""Test resolution manager properties.""" """Test resolution manager properties unsupported."""
assert coresys.core.supported assert coresys.core.supported
@ -34,6 +35,15 @@ def test_properies(coresys: CoreSys):
assert not coresys.core.supported assert not coresys.core.supported
def test_properies_unhealthy(coresys: CoreSys):
"""Test resolution manager properties unhealthy."""
assert coresys.core.healthy
coresys.resolution.unhealthy = UnhealthyReason.SUPERVISOR
assert not coresys.core.healthy
async def test_clear_snapshots(coresys: CoreSys, tmp_path): async def test_clear_snapshots(coresys: CoreSys, tmp_path):
"""Test snapshot cleanup.""" """Test snapshot cleanup."""
for slug in ["sn1", "sn2", "sn3", "sn4", "sn5"]: for slug in ["sn1", "sn2", "sn3", "sn4", "sn5"]: