Add repair for detached addon issues (#118064)
Co-authored-by: Franck Nijhof <git@frenck.dev>pull/117667/head
parent
233c3bb2be
commit
05c24e92d1
|
@ -97,10 +97,14 @@ DATA_KEY_CORE = "core"
|
|||
DATA_KEY_HOST = "host"
|
||||
DATA_KEY_SUPERVISOR_ISSUES = "supervisor_issues"
|
||||
|
||||
PLACEHOLDER_KEY_ADDON = "addon"
|
||||
PLACEHOLDER_KEY_ADDON_URL = "addon_url"
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
PLACEHOLDER_KEY_COMPONENTS = "components"
|
||||
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
|
||||
CORE_CONTAINER = "homeassistant"
|
||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME
|
||||
|
@ -53,7 +53,9 @@ from .const import (
|
|||
SupervisorEntityModel,
|
||||
)
|
||||
from .handler import HassIO, HassioAPIError
|
||||
from .issues import SupervisorIssues
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .issues import SupervisorIssues
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -36,12 +36,17 @@ from .const import (
|
|||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_ADDON_URL,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .coordinator import get_addons_info
|
||||
from .handler import HassIO, HassioAPIError
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
|
@ -93,6 +98,8 @@ ISSUE_KEYS_FOR_REPAIRS = {
|
|||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -258,6 +265,20 @@ class SupervisorIssues:
|
|||
placeholders: dict[str, str] | None = None
|
||||
if issue.reference:
|
||||
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
|
||||
|
||||
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
|
||||
addons = get_addons_info(self._hass)
|
||||
if addons and issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
|
||||
"name"
|
||||
]
|
||||
if "url" in addons[issue.reference]:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
|
||||
issue.reference
|
||||
]["url"]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
|
|
|
@ -14,7 +14,9 @@ from homeassistant.data_entry_flow import FlowResult
|
|||
|
||||
from . import get_addons_info, get_issues_info
|
||||
from .const import (
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_COMPONENTS,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
SupervisorIssueContext,
|
||||
|
@ -22,12 +24,23 @@ from .const import (
|
|||
from .handler import async_apply_suggestion
|
||||
from .issues import Issue, Suggestion
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {"system_adopt_data_disk", "system_execute_reboot"}
|
||||
HELP_URLS = {
|
||||
"help_url": "https://www.home-assistant.io/help/",
|
||||
"community_url": "https://community.home-assistant.io/",
|
||||
}
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {
|
||||
"addon_execute_remove",
|
||||
"system_adopt_data_disk",
|
||||
"system_execute_reboot",
|
||||
}
|
||||
|
||||
|
||||
EXTRA_PLACEHOLDERS = {
|
||||
"issue_mount_mount_failed": {
|
||||
"storage_url": "/config/storage",
|
||||
}
|
||||
},
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
|
||||
}
|
||||
|
||||
|
||||
|
@ -168,6 +181,25 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
|||
return placeholders
|
||||
|
||||
|
||||
class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for detached addon issue fixing flows."""
|
||||
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
placeholders: dict[str, str] = super().description_placeholders or {}
|
||||
if self.issue and self.issue.reference:
|
||||
addons = get_addons_info(self.hass)
|
||||
if addons and self.issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[self.issue.reference][
|
||||
"name"
|
||||
]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = self.issue.reference
|
||||
|
||||
return placeholders or None
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
|
@ -178,5 +210,7 @@ async def async_create_fix_flow(
|
|||
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
||||
return DockerConfigIssueRepairFlow(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
|
||||
return DetachedAddonIssueRepairFlow(issue_id)
|
||||
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
||||
|
|
|
@ -17,6 +17,23 @@
|
|||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_addon_detached_addon_missing": {
|
||||
"title": "Missing repository for an installed add-on",
|
||||
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
|
||||
},
|
||||
"issue_addon_detached_addon_removed": {
|
||||
"title": "Installed add-on has been removed from repository",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"addon_execute_remove": {
|
||||
"description": "Add-on {addon} has been removed from the repository it was installed from. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nClicking submit will uninstall this deprecated add-on. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not uninstall the add-on. Check the Supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_mount_mount_failed": {
|
||||
"title": "Network storage device failed",
|
||||
"fix_flow": {
|
||||
|
|
|
@ -27,11 +27,6 @@ async def setup_repairs(hass):
|
|||
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_all(all_setup_requests):
|
||||
"""Mock all setup requests."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def fixture_supervisor_environ():
|
||||
"""Mock os environ for supervisor."""
|
||||
|
@ -110,9 +105,13 @@ def assert_issue_repair_in_list(
|
|||
context: str,
|
||||
type_: str,
|
||||
fixable: bool,
|
||||
reference: str | None,
|
||||
*,
|
||||
reference: str | None = None,
|
||||
placeholders: dict[str, str] | None = None,
|
||||
):
|
||||
"""Assert repair for unhealthy/unsupported in list."""
|
||||
if reference:
|
||||
placeholders = (placeholders or {}) | {"reference": reference}
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
|
@ -125,7 +124,7 @@ def assert_issue_repair_in_list(
|
|||
"learn_more_url": None,
|
||||
"severity": "warning",
|
||||
"translation_key": f"issue_{context}_{type_}",
|
||||
"translation_placeholders": {"reference": reference} if reference else None,
|
||||
"translation_placeholders": placeholders,
|
||||
} in issues
|
||||
|
||||
|
||||
|
@ -133,6 +132,7 @@ async def test_unhealthy_issues(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues added for unhealthy systems."""
|
||||
mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"])
|
||||
|
@ -154,6 +154,7 @@ async def test_unsupported_issues(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues added for unsupported systems."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"])
|
||||
|
@ -177,6 +178,7 @@ async def test_unhealthy_issues_add_remove(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test unhealthy issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
@ -233,6 +235,7 @@ async def test_unsupported_issues_add_remove(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test unsupported issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
@ -289,6 +292,7 @@ async def test_reset_issues_supervisor_restart(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""All issues reset on supervisor restart."""
|
||||
mock_resolution_info(
|
||||
|
@ -352,6 +356,7 @@ async def test_reasons_added_and_removed(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
|
@ -401,6 +406,7 @@ async def test_ignored_unsupported_skipped(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Unsupported reasons which have an identical unhealthy reason are ignored."""
|
||||
mock_resolution_info(
|
||||
|
@ -423,6 +429,7 @@ async def test_new_unsupported_unhealthy_reason(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""New unsupported/unhealthy reasons result in a generic repair until next core update."""
|
||||
mock_resolution_info(
|
||||
|
@ -472,6 +479,7 @@ async def test_supervisor_issues(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test repairs added for supervisor issue."""
|
||||
mock_resolution_info(
|
||||
|
@ -538,6 +546,7 @@ async def test_supervisor_issues_initial_failure(
|
|||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues manager retries after initial update failure."""
|
||||
responses = [
|
||||
|
@ -614,6 +623,7 @@ async def test_supervisor_issues_add_remove(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test supervisor issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
@ -724,6 +734,7 @@ async def test_supervisor_issues_suggestions_fail(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test failing to get suggestions for issue skips it."""
|
||||
aioclient_mock.get(
|
||||
|
@ -769,6 +780,7 @@ async def test_supervisor_remove_missing_issue_without_error(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test HA skips message to remove issue that it didn't know about (sync issue)."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
@ -802,6 +814,7 @@ async def test_system_is_not_ready(
|
|||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Ensure hassio starts despite error."""
|
||||
aioclient_mock.get(
|
||||
|
@ -814,3 +827,57 @@ async def test_system_is_not_ready(
|
|||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
assert "Failed to update supervisor issues" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
async def test_supervisor_issues_detached_addon_missing(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test supervisor issue for detached addon due to missing repository."""
|
||||
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": "issue_changed",
|
||||
"data": {
|
||||
"uuid": "1234",
|
||||
"type": "detached_addon_missing",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
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_issue_repair_in_list(
|
||||
msg["result"]["issues"],
|
||||
uuid="1234",
|
||||
context="addon",
|
||||
type_="detached_addon_missing",
|
||||
fixable=False,
|
||||
placeholders={
|
||||
"reference": "test",
|
||||
"addon": "test",
|
||||
"addon_url": "https://github.com/home-assistant/addons/test",
|
||||
},
|
||||
)
|
||||
|
|
|
@ -780,3 +780,90 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks(
|
|||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1236"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
async def test_supervisor_issue_detached_addon_removed(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test fix flow for supervisor issue."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock,
|
||||
issues=[
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "detached_addon_removed",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": "1235",
|
||||
"type": "execute_remove",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
|
||||
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
assert repair_issue
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post(
|
||||
"/api/repairs/issues/fix",
|
||||
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "form",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"step_id": "addon_execute_remove",
|
||||
"data_schema": [],
|
||||
"errors": None,
|
||||
"description_placeholders": {
|
||||
"reference": "test",
|
||||
"addon": "test",
|
||||
"help_url": "https://www.home-assistant.io/help/",
|
||||
"community_url": "https://community.home-assistant.io/",
|
||||
},
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "create_entry",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"description": None,
|
||||
"description_placeholders": None,
|
||||
}
|
||||
|
||||
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
|
||||
assert aioclient_mock.mock_calls[-1][0] == "post"
|
||||
assert (
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1235"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue