mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add move local data option to hassio mount failed repair (#179369)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ace0e5a301
commit
840d981676
@@ -40,7 +40,7 @@ from homeassistant.core import (
|
||||
callback,
|
||||
is_callback_check_partial,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -399,16 +399,28 @@ class SupervisorIssuesCoordinator(DataUpdateCoordinator[SupervisorIssuesData]):
|
||||
current_data: SupervisorIssuesData,
|
||||
) -> None:
|
||||
"""Create/delete issue repairs and notify subscribers based on issue deltas."""
|
||||
issue_registry = ir.async_get(self.hass)
|
||||
for issue in current_data.issues.values():
|
||||
previous_issue = previous_data.issues.get(issue.uuid)
|
||||
if previous_issue is not None and self._issue_equal(previous_issue, issue):
|
||||
continue
|
||||
|
||||
self._create_or_update_issue_repair(issue)
|
||||
self._process_issue_change(
|
||||
IssueSubscriptionEvent(event="changed", issue=issue)
|
||||
changed = previous_issue is None or not self._issue_equal(
|
||||
previous_issue, issue
|
||||
)
|
||||
|
||||
# Update the repair on changes, and re-create it if the registry
|
||||
# entry went missing: a finished repair flow deletes the entry
|
||||
# even when applying the suggestion failed in Supervisor and the
|
||||
# issue is unchanged.
|
||||
if changed or (
|
||||
issue.key in ISSUE_KEYS_FOR_REPAIRS
|
||||
and not issue_registry.async_get_issue(DOMAIN, issue.uuid.hex)
|
||||
):
|
||||
self._create_or_update_issue_repair(issue)
|
||||
|
||||
if changed:
|
||||
self._process_issue_change(
|
||||
IssueSubscriptionEvent(event="changed", issue=issue)
|
||||
)
|
||||
|
||||
for issue_uuid, issue in previous_data.issues.items():
|
||||
if issue_uuid not in current_data.issues:
|
||||
self._delete_issue_repair(issue)
|
||||
|
||||
@@ -41,6 +41,7 @@ from .issues import Issue, Suggestion
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {
|
||||
"addon_execute_remove",
|
||||
"mount_move_local_data",
|
||||
"system_adopt_data_disk",
|
||||
"system_execute_reboot",
|
||||
}
|
||||
|
||||
@@ -152,11 +152,15 @@
|
||||
},
|
||||
"step": {
|
||||
"fix_menu": {
|
||||
"description": "Could not connect to `{reference}`. Check host logs for errors from the mount service for more details.\n\nUse reload to try to connect again. If you need to update `{reference}`, go to [storage]({storage_url}).",
|
||||
"description": "Could not set up `{reference}`. This can happen when the storage device is unreachable, or when local data is blocking the mount location. Check host logs for errors from the mount service for more details.\n\nUse reload to try again. If you need to update `{reference}`, go to [storage]({storage_url}).",
|
||||
"menu_options": {
|
||||
"mount_execute_reload": "[%key:common::action::reload%]",
|
||||
"mount_execute_remove": "Remove"
|
||||
"mount_execute_remove": "Remove",
|
||||
"mount_move_local_data": "Move blocking local data away"
|
||||
}
|
||||
},
|
||||
"mount_move_local_data": {
|
||||
"description": "Select **Submit** to move the local data blocking `{reference}` to a `{reference}_local_recovery` folder and set up the mount again.\n\nNo files are deleted. For media and share mounts the recovery folder is created next to the mount, where you can review and remove it. For backup mounts it is placed in local backup storage."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ from homeassistant.components.hassio.coordinator import (
|
||||
)
|
||||
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -1436,6 +1437,59 @@ async def test_supervisor_issues_periodic_refresh_backstop(
|
||||
supervisor_client.resolution.info.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_issue_repair_recreated_when_registry_entry_missing(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test a repair deleted from the registry is re-created for an unchanged issue.
|
||||
|
||||
A finished repair flow deletes the issue registry entry even when applying
|
||||
the suggestion failed in Supervisor. The issue then comes back unchanged on
|
||||
the next refresh and must be re-created instead of skipped as known.
|
||||
"""
|
||||
mock_resolution_info(
|
||||
supervisor_client,
|
||||
issues=[
|
||||
Issue(
|
||||
type=IssueType.MOUNT_FAILED,
|
||||
context=ContextType.MOUNT,
|
||||
reference="m1",
|
||||
uuid=(issue_uuid := uuid4()),
|
||||
reference_extra=None,
|
||||
)
|
||||
],
|
||||
suggestions_by_issue={
|
||||
issue_uuid: [
|
||||
Suggestion(
|
||||
SuggestionType.EXECUTE_RELOAD,
|
||||
context=ContextType.MOUNT,
|
||||
reference="m1",
|
||||
uuid=uuid4(),
|
||||
auto=False,
|
||||
reference_extra=None,
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, DOMAIN, {})
|
||||
assert result
|
||||
|
||||
assert issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_uuid.hex)
|
||||
|
||||
# Simulate a finished repair flow whose suggestion failed to apply in
|
||||
# Supervisor: the registry entry is gone, the supervisor issue unchanged
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_uuid.hex)
|
||||
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_uuid.hex)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.utcnow() + HASSIO_ISSUES_UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_uuid.hex)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_supervisor_issues_suggestions_change_updates_fixable_state(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -749,6 +749,106 @@ async def test_mount_failed_repair_flow(
|
||||
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_mount_failed_move_local_data_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test moving blocking local data from the mount_failed repair."""
|
||||
mock_resolution_info(
|
||||
supervisor_client,
|
||||
issues=[
|
||||
Issue(
|
||||
type=IssueType.MOUNT_FAILED,
|
||||
context=ContextType.MOUNT,
|
||||
reference="media_share",
|
||||
uuid=(issue_uuid := uuid4()),
|
||||
reference_extra=None,
|
||||
),
|
||||
],
|
||||
suggestions_by_issue={
|
||||
issue_uuid: [
|
||||
Suggestion(
|
||||
# Not in aiohasupervisor's SuggestionType enum yet, arrives
|
||||
# as a plain string like any newer supervisor suggestion
|
||||
type="move_local_data",
|
||||
context=ContextType.MOUNT,
|
||||
reference="media_share",
|
||||
uuid=(sugg_uuid := uuid4()),
|
||||
auto=False,
|
||||
reference_extra=None,
|
||||
),
|
||||
Suggestion(
|
||||
type=SuggestionType.EXECUTE_RELOAD,
|
||||
context=ContextType.MOUNT,
|
||||
reference="media_share",
|
||||
uuid=uuid4(),
|
||||
auto=False,
|
||||
reference_extra=None,
|
||||
),
|
||||
Suggestion(
|
||||
type=SuggestionType.EXECUTE_REMOVE,
|
||||
context=ContextType.MOUNT,
|
||||
reference="media_share",
|
||||
uuid=uuid4(),
|
||||
auto=False,
|
||||
reference_extra=None,
|
||||
),
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
|
||||
repair_issue = issue_registry.async_get_issue(
|
||||
domain="hassio", issue_id=issue_uuid.hex
|
||||
)
|
||||
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"] == "menu"
|
||||
assert data["menu_options"] == [
|
||||
"mount_move_local_data",
|
||||
"mount_execute_reload",
|
||||
"mount_execute_remove",
|
||||
]
|
||||
|
||||
# Moving data aside requires a confirmation step
|
||||
resp = await client.post(
|
||||
f"/api/repairs/issues/fix/{flow_id}",
|
||||
json={"next_step_id": "mount_move_local_data"},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["type"] == "form"
|
||||
assert data["step_id"] == "mount_move_local_data"
|
||||
supervisor_client.resolution.apply_suggestion.assert_not_called()
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}", json={})
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
assert data["type"] == "create_entry"
|
||||
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
|
||||
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user