mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 07:06:30 +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,
|
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(
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
@ -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:
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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]
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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"]:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user