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,
HostAppArmorError,
)
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..store.addon import AddonStore
from ..utils import check_exception_chain
from .addon import Addon
@ -376,7 +377,12 @@ class AddonManager(CoreSysAttributes):
continue
except DockerError as 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)
else:
self.sys_plugins.dns.add_host(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ from .const import (
ContextType,
IssueType,
SuggestionType,
UnhealthyReason,
UnsupportedReason,
)
from .data import Issue, Suggestion
@ -32,6 +33,7 @@ class ResolutionManager(CoreSysAttributes):
self._suggestions: List[Suggestion] = []
self._issues: List[Issue] = []
self._unsupported: List[UnsupportedReason] = []
self._unhealthy: List[UnhealthyReason] = []
@property
def evaluate(self) -> ResolutionEvaluation:
@ -81,6 +83,17 @@ class ResolutionManager(CoreSysAttributes):
if reason not in self._unsupported:
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:
"""Return suggestion with uuid."""
for suggestion in self._suggestions:

View File

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

View File

@ -2,7 +2,7 @@
import logging
from ..coresys import CoreSys, CoreSysAttributes
from .const import UnsupportedReason
from .const import UnhealthyReason, UnsupportedReason
from .evaluations.container import EvaluateContainer
from .evaluations.dbus import EvaluateDbus
from .evaluations.docker_configuration import EvaluateDockerConfiguration
@ -54,6 +54,6 @@ class ResolutionEvaluation(CoreSysAttributes):
await self._systemd()
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")

View File

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

View File

@ -3,13 +3,19 @@ from unittest.mock import AsyncMock
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.exceptions import ResolutionError
from supervisor.resolution.const import (
ContextType,
IssueType,
SuggestionType,
UnhealthyReason,
UnsupportedReason,
)
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
await api_client.delete(f"/resolution/issue/{updated_failed.uuid}")
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.coresys import CoreSys
from supervisor.jobs.decorator import Job, JobCondition
from supervisor.resolution.const import UnhealthyReason
async def test_healthy(coresys: CoreSys):
@ -25,7 +26,7 @@ async def test_healthy(coresys: CoreSys):
test = TestClass(coresys)
assert await test.execute()
coresys.core.healthy = False
coresys.resolution.unhealthy = UnhealthyReason.DOCKER
assert not await test.execute()

View File

@ -7,7 +7,12 @@ import pytest
from supervisor.const import SUPERVISOR_VERSION, CoreState
from supervisor.exceptions import AddonConfigurationError
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"}}
@ -117,3 +122,17 @@ def test_issues_on_report(coresys):
assert "issues" in event["contexts"]
assert event["contexts"]["issues"][0]["type"] == IssueType.FATAL_ERROR
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,
IssueType,
SuggestionType,
UnhealthyReason,
UnsupportedReason,
)
from supervisor.resolution.data import Issue, Suggestion
@ -25,8 +26,8 @@ from supervisor.utils.dt import utcnow
from supervisor.utils.tar import SecureTarFile
def test_properies(coresys: CoreSys):
"""Test resolution manager properties."""
def test_properies_unsupported(coresys: CoreSys):
"""Test resolution manager properties unsupported."""
assert coresys.core.supported
@ -34,6 +35,15 @@ def test_properies(coresys: CoreSys):
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):
"""Test snapshot cleanup."""
for slug in ["sn1", "sn2", "sn3", "sn4", "sn5"]: