mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-18 22:56:31 +00:00
Handle Unhealthy like Unsupported (#2255)
* Handle Unhealthy like Unsupported * Add tests * Add unhealthy to sentry * Add test
This commit is contained in:
parent
7ee5737f75
commit
2040102e21
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"]:
|
||||
|
Loading…
x
Reference in New Issue
Block a user