Create repairs for unsupported and unhealthy (#80747)
parent
1589c06203
commit
82151bfd40
|
@ -77,6 +77,7 @@ from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F4
|
|||
from .handler import HassIO, HassioAPIError, api_data
|
||||
from .http import HassIOView
|
||||
from .ingress import async_setup_ingress_view
|
||||
from .repairs import SupervisorRepairs
|
||||
from .websocket_api import async_load_websocket_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -103,6 +104,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
|||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_SUPERVISOR_REPAIRS = "supervisor_repairs"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
|
@ -758,6 +760,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||
)
|
||||
|
||||
# Start listening for problems with supervisor and making repairs
|
||||
hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio)
|
||||
await repairs.setup()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -11,19 +11,26 @@ ATTR_CONFIG = "config"
|
|||
ATTR_DATA = "data"
|
||||
ATTR_DISCOVERY = "discovery"
|
||||
ATTR_ENABLE = "enable"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_FOLDERS = "folders"
|
||||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_HOMEASSISTANT = "homeassistant"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_PANELS = "panels"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_TITLE = "title"
|
||||
ATTR_UNHEALTHY = "unhealthy"
|
||||
ATTR_UNHEALTHY_REASONS = "unhealthy_reasons"
|
||||
ATTR_UNSUPPORTED = "unsupported"
|
||||
ATTR_UNSUPPORTED_REASONS = "unsupported_reasons"
|
||||
ATTR_UPDATE_KEY = "update_key"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_WS_EVENT = "event"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
|
||||
X_AUTH_TOKEN = "X-Supervisor-Token"
|
||||
X_INGRESS_PATH = "X-Ingress-Path"
|
||||
|
@ -38,6 +45,11 @@ WS_TYPE_EVENT = "supervisor/event"
|
|||
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||
EVENT_SUPERVISOR_UPDATE = "supervisor_update"
|
||||
EVENT_HEALTH_CHANGED = "health_changed"
|
||||
EVENT_SUPPORTED_CHANGED = "supported_changed"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
|
@ -51,7 +63,6 @@ ATTR_STARTED = "started"
|
|||
ATTR_URL = "url"
|
||||
ATTR_REPOSITORY = "repository"
|
||||
|
||||
|
||||
DATA_KEY_ADDONS = "addons"
|
||||
DATA_KEY_OS = "os"
|
||||
DATA_KEY_SUPERVISOR = "supervisor"
|
||||
|
|
|
@ -190,6 +190,14 @@ class HassIO:
|
|||
"""
|
||||
return self.send_command(f"/discovery/{uuid}", method="get")
|
||||
|
||||
@api_data
|
||||
def get_resolution_info(self):
|
||||
"""Return data for Supervisor resolution center.
|
||||
|
||||
This method return a coroutine.
|
||||
"""
|
||||
return self.send_command("/resolution/info", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(self, http_config, refresh_token):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
"""Supervisor events monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_UNHEALTHY,
|
||||
ATTR_UNHEALTHY_REASONS,
|
||||
ATTR_UNSUPPORTED,
|
||||
ATTR_UNSUPPORTED_REASONS,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
DOMAIN,
|
||||
EVENT_HEALTH_CHANGED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .handler import HassIO
|
||||
|
||||
ISSUE_ID_UNHEALTHY = "unhealthy_system"
|
||||
ISSUE_ID_UNSUPPORTED = "unsupported_system"
|
||||
|
||||
INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy"
|
||||
INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported"
|
||||
|
||||
|
||||
class SupervisorRepairs:
|
||||
"""Create repairs from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
"""Initialize supervisor repairs."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
"""Get unhealthy reasons. Returns empty set if system is healthy."""
|
||||
return self._unhealthy_reasons
|
||||
|
||||
@unhealthy_reasons.setter
|
||||
def unhealthy_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unhealthy reasons. Create or delete repairs as necessary."""
|
||||
for unhealthy in reasons - self.unhealthy_reasons:
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNHEALTHY}_{unhealthy}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
translation_key="unhealthy",
|
||||
translation_placeholders={"reason": unhealthy},
|
||||
)
|
||||
|
||||
for fixed in self.unhealthy_reasons - reasons:
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}")
|
||||
|
||||
self._unhealthy_reasons = reasons
|
||||
|
||||
@property
|
||||
def unsupported_reasons(self) -> set[str]:
|
||||
"""Get unsupported reasons. Returns empty set if system is supported."""
|
||||
return self._unsupported_reasons
|
||||
|
||||
@unsupported_reasons.setter
|
||||
def unsupported_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unsupported reasons. Create or delete repairs as necessary."""
|
||||
for unsupported in reasons - self.unsupported_reasons:
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNSUPPORTED}_{unsupported}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="unsupported",
|
||||
translation_placeholders={"reason": unsupported},
|
||||
)
|
||||
|
||||
for fixed in self.unsupported_reasons - reasons:
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}")
|
||||
|
||||
self._unsupported_reasons = reasons
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
await self.update()
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs
|
||||
)
|
||||
|
||||
async def update(self) -> None:
|
||||
"""Update repairs from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
@callback
|
||||
def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None:
|
||||
"""Create repairs from supervisor events."""
|
||||
if ATTR_WS_EVENT not in event:
|
||||
return
|
||||
|
||||
if (
|
||||
event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
|
||||
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
|
||||
):
|
||||
self._hass.async_create_task(self.update())
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED:
|
||||
self.unhealthy_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_HEALTHY]
|
||||
else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS])
|
||||
)
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED:
|
||||
self.unsupported_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_SUPPORTED]
|
||||
else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
|
||||
)
|
|
@ -15,5 +15,15 @@
|
|||
"update_channel": "Update Channel",
|
||||
"version_api": "Version API"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Unsupported system - {reason}",
|
||||
"description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,19 @@ def mock_all(aioclient_mock, request):
|
|||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request):
|
|||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_diagnostics(
|
||||
|
|
|
@ -183,6 +183,19 @@ def mock_all(aioclient_mock, request, os_info):
|
|||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_api_ping(hass, aioclient_mock):
|
||||
|
@ -191,7 +204,7 @@ async def test_setup_api_ping(hass, aioclient_mock):
|
|||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
|
||||
assert hass.components.hassio.is_hassio()
|
||||
|
||||
|
@ -230,7 +243,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock):
|
|||
)
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||
assert aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||
|
@ -246,7 +259,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
|
|||
)
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
|
||||
|
@ -258,7 +271,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag
|
|||
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
|
||||
|
@ -325,7 +338,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage
|
|||
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
|
||||
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
|
||||
|
@ -339,7 +352,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock):
|
|||
result = await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
|
||||
|
||||
with patch("homeassistant.util.dt.set_default_time_zone"):
|
||||
|
@ -356,7 +369,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock):
|
|||
result = await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
assert result
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
|
||||
|
||||
|
||||
|
@ -426,14 +439,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 9
|
||||
assert aioclient_mock.call_count == 10
|
||||
assert aioclient_mock.mock_calls[-1][2] == "test"
|
||||
|
||||
await hass.services.async_call("hassio", "host_shutdown", {})
|
||||
await hass.services.async_call("hassio", "host_reboot", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 11
|
||||
assert aioclient_mock.call_count == 12
|
||||
|
||||
await hass.services.async_call("hassio", "backup_full", {})
|
||||
await hass.services.async_call(
|
||||
|
@ -448,7 +461,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 13
|
||||
assert aioclient_mock.call_count == 14
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"homeassistant": True,
|
||||
"addons": ["test"],
|
||||
|
@ -472,7 +485,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog):
|
|||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"addons": ["test"],
|
||||
"folders": ["ssl"],
|
||||
|
@ -491,12 +504,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
|||
await hass.services.async_call("homeassistant", "stop")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 5
|
||||
assert aioclient_mock.call_count == 6
|
||||
|
||||
await hass.services.async_call("homeassistant", "check_config")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 5
|
||||
assert aioclient_mock.call_count == 6
|
||||
|
||||
with patch(
|
||||
"homeassistant.config.async_check_ha_config_file", return_value=None
|
||||
|
@ -505,7 +518,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock):
|
|||
await hass.async_block_till_done()
|
||||
assert mock_check_config.called
|
||||
|
||||
assert aioclient_mock.call_count == 6
|
||||
assert aioclient_mock.call_count == 7
|
||||
|
||||
|
||||
async def test_entry_load_and_unload(hass):
|
||||
|
@ -758,7 +771,7 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration):
|
|||
assert result
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 15
|
||||
assert aioclient_mock.call_count == 16
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,395 @@
|
|||
"""Test repairs from supervisor issues."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_init import MOCK_ENVIRON
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_repairs(hass):
|
||||
"""Set up the repairs integration."""
|
||||
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all(aioclient_mock: AiohttpClientMocker, request: pytest.FixtureRequest):
|
||||
"""Mock all setup requests."""
|
||||
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
|
||||
aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"})
|
||||
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"supervisor": "222",
|
||||
"homeassistant": "0.110.0",
|
||||
"hassos": "1.2.3",
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/store",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {"addons": [], "repositories": []},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/host/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"chassis": "vm",
|
||||
"operating_system": "Debian GNU/Linux 10 (buster)",
|
||||
"kernel": "4.19.0-6-amd64",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/core/info",
|
||||
json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/os/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"version_latest": "1.0.0",
|
||||
"version": "1.0.0",
|
||||
"update_available": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/supervisor/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"result": "ok",
|
||||
"version": "1.0.0",
|
||||
"version_latest": "1.0.0",
|
||||
"auto_update": True,
|
||||
"addons": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def fixture_supervisor_environ():
|
||||
"""Mock os environ for supervisor."""
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
yield
|
||||
|
||||
|
||||
def mock_resolution_info(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
unsupported: list[str] | None = None,
|
||||
unhealthy: list[str] | None = None,
|
||||
):
|
||||
"""Mock resolution/info endpoint with unsupported/unhealthy reasons."""
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": unsupported or [],
|
||||
"unhealthy": unhealthy or [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [
|
||||
{"enabled": True, "slug": "supervisor_trust"},
|
||||
{"enabled": True, "slug": "free_space"},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str):
|
||||
"""Assert repair for unhealthy/unsupported in list."""
|
||||
repair_type = "unhealthy" if unhealthy else "unsupported"
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
"dismissed_version": None,
|
||||
"domain": "hassio",
|
||||
"ignored": False,
|
||||
"is_fixable": False,
|
||||
"issue_id": f"{repair_type}_system_{reason}",
|
||||
"issue_domain": None,
|
||||
"learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}",
|
||||
"severity": "critical" if unhealthy else "warning",
|
||||
"translation_key": repair_type,
|
||||
"translation_placeholders": {
|
||||
"reason": reason,
|
||||
},
|
||||
} in issues
|
||||
|
||||
|
||||
async def test_unhealthy_repairs(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test repairs added for unhealthy systems."""
|
||||
mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
|
||||
|
||||
|
||||
async def test_unsupported_repairs(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test repairs added for unsupported systems."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(
|
||||
msg["result"]["issues"], unhealthy=False, reason="content_trust"
|
||||
)
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
|
||||
async def test_unhealthy_repairs_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test unhealthy repairs added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "health_changed",
|
||||
"data": {
|
||||
"healthy": False,
|
||||
"unhealthy_reasons": ["docker"],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "health_changed",
|
||||
"data": {"healthy": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 4, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_unsupported_repairs_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test unsupported repairs added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supported_changed",
|
||||
"data": {
|
||||
"supported": False,
|
||||
"unsupported_reasons": ["os"],
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 3,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supported_changed",
|
||||
"data": {"supported": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 4, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_reset_repairs_supervisor_restart(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Unsupported/unhealthy repairs reset on supervisor restart."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
mock_resolution_info(aioclient_mock)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supervisor_update",
|
||||
"update_key": "supervisor",
|
||||
"data": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 3, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"issues": []}
|
||||
|
||||
|
||||
async def test_reasons_added_and_removed(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client,
|
||||
):
|
||||
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json({"id": 1, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker")
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os")
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
mock_resolution_info(
|
||||
aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"]
|
||||
)
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 2,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "supervisor_update",
|
||||
"update_key": "supervisor",
|
||||
"data": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 3, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 2
|
||||
assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup")
|
||||
assert_repair_in_list(
|
||||
msg["result"]["issues"], unhealthy=False, reason="content_trust"
|
||||
)
|
|
@ -126,6 +126,19 @@ def mock_all(aioclient_mock, request):
|
|||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request):
|
|||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -61,6 +61,19 @@ def mock_all(aioclient_mock):
|
|||
aioclient_mock.get(
|
||||
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
|
||||
)
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client):
|
||||
|
|
|
@ -198,7 +198,17 @@ async def test_access_from_supervisor_ip(
|
|||
|
||||
manager: IpBanManager = app[KEY_BAN_MANAGER]
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
with patch(
|
||||
"homeassistant.components.hassio.HassIO.get_resolution_info",
|
||||
return_value={
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, "hassio", {"hassio": {}})
|
||||
|
||||
m_open = mock_open()
|
||||
|
||||
|
|
|
@ -57,6 +57,19 @@ async def mock_supervisor_fixture(hass, aioclient_mock):
|
|||
"""Mock supervisor."""
|
||||
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
|
||||
aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"})
|
||||
aioclient_mock.get(
|
||||
"http://127.0.0.1/resolution/info",
|
||||
json={
|
||||
"result": "ok",
|
||||
"data": {
|
||||
"unsupported": [],
|
||||
"unhealthy": [],
|
||||
"suggestions": [],
|
||||
"issues": [],
|
||||
"checks": [],
|
||||
},
|
||||
},
|
||||
)
|
||||
with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch(
|
||||
"homeassistant.components.hassio.HassIO.is_connected",
|
||||
return_value=True,
|
||||
|
|
Loading…
Reference in New Issue