From 82151bfd40f7b64044a5a9f82f020411430df97b Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 31 Oct 2022 09:57:54 -0400 Subject: [PATCH] Create repairs for unsupported and unhealthy (#80747) --- homeassistant/components/hassio/__init__.py | 6 + homeassistant/components/hassio/const.py | 21 +- homeassistant/components/hassio/handler.py | 8 + homeassistant/components/hassio/repairs.py | 138 ++++++ homeassistant/components/hassio/strings.json | 10 + tests/components/hassio/test_binary_sensor.py | 13 + tests/components/hassio/test_diagnostics.py | 13 + tests/components/hassio/test_init.py | 43 +- tests/components/hassio/test_repairs.py | 395 ++++++++++++++++++ tests/components/hassio/test_sensor.py | 13 + tests/components/hassio/test_update.py | 13 + tests/components/hassio/test_websocket_api.py | 13 + tests/components/http/test_ban.py | 12 +- tests/components/onboarding/test_views.py | 13 + 14 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/hassio/repairs.py create mode 100644 tests/components/hassio/test_repairs.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8535a0c3cc6..c811b35812e 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -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 diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e37a31ddbd6..64ef7a718a5 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -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" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7b3ed697227..ee16bdf8158 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -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.""" diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py new file mode 100644 index 00000000000..a8c6788f4d5 --- /dev/null +++ b/homeassistant/components/hassio/repairs.py @@ -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]) + ) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 90142bd453f..81b5ce01b79 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -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." + } } } diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index a601f98f1c5..c2dab178ad8 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -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( diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 1f915e17e61..9eaaf5f97d9 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -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( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f0f94661d50..371398e32c9 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -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 diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py new file mode 100644 index 00000000000..ebaf46be3b5 --- /dev/null +++ b/tests/components/hassio/test_repairs.py @@ -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" + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 16cce09b800..e9f0bd631b0 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -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( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index aaa77cde129..02d6b1dbf6b 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -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( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5d11d13166e..767f0abaf35 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -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): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 7a4202c1a67..a4249a1efb6 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -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() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 204eb6bf772..40d889185dd 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -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,