mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add backup location and mount failed repair (#93126)
* Add backup location and mount failed repair * Fix coverage * Change storage_location to backup_location * Use backticks * Update homeassistant/helpers/selector.py --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
9b53484e2e
commit
08a719e09e
@ -61,6 +61,7 @@ from .const import (
|
||||
ATTR_FOLDERS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_INPUT,
|
||||
ATTR_LOCATION,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SLUG,
|
||||
@ -161,6 +162,9 @@ SCHEMA_BACKUP_FULL = vol.Schema(
|
||||
vol.Optional(ATTR_NAME): cv.string,
|
||||
vol.Optional(ATTR_PASSWORD): cv.string,
|
||||
vol.Optional(ATTR_COMPRESSED): cv.boolean,
|
||||
vol.Optional(ATTR_LOCATION): vol.All(
|
||||
cv.string, lambda v: None if v == "/backup" else v
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -61,6 +61,7 @@ ATTR_VERSION = "version"
|
||||
ATTR_VERSION_LATEST = "version_latest"
|
||||
ATTR_CPU_PERCENT = "cpu_percent"
|
||||
ATTR_CHANGELOG = "changelog"
|
||||
ATTR_LOCATION = "location"
|
||||
ATTR_MEMORY_PERCENT = "memory_percent"
|
||||
ATTR_SLUG = "slug"
|
||||
ATTR_STATE = "state"
|
||||
|
@ -85,6 +85,7 @@ UNHEALTHY_REASONS = {
|
||||
|
||||
# Keys (type + context) of issues that when found should be made into a repair
|
||||
ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_mount_mount_failed",
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
}
|
||||
|
@ -16,6 +16,12 @@ from .issues import Issue, Suggestion, SupervisorIssues
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"}
|
||||
|
||||
EXTRA_PLACEHOLDERS = {
|
||||
"issue_mount_mount_failed": {
|
||||
"storage_url": "/config/storage",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
@ -42,11 +48,13 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
return (
|
||||
{PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
if self.issue and self.issue.reference
|
||||
else None
|
||||
)
|
||||
placeholders = {}
|
||||
if self.issue:
|
||||
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
|
||||
if self.issue.reference:
|
||||
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
|
||||
return placeholders or None
|
||||
|
||||
def _async_form_for_suggestion(self, suggestion: Suggestion) -> FlowResult:
|
||||
"""Return form for suggestion."""
|
||||
|
@ -88,6 +88,12 @@ backup_full:
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
location:
|
||||
name: Location
|
||||
description: Name of a backup network storage to put backup (or /backup)
|
||||
example: my_backup_mount
|
||||
selector:
|
||||
backup_location:
|
||||
|
||||
backup_partial:
|
||||
name: Create a partial backup.
|
||||
@ -128,6 +134,12 @@ backup_partial:
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
location:
|
||||
name: Location
|
||||
description: Name of a backup network storage to put backup (or /backup)
|
||||
example: my_backup_mount
|
||||
selector:
|
||||
backup_location:
|
||||
|
||||
restore_full:
|
||||
name: Restore from full backup.
|
||||
|
@ -17,6 +17,23 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_mount_mount_failed": {
|
||||
"title": "Network storage device failed",
|
||||
"fix_flow": {
|
||||
"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}).",
|
||||
"menu_options": {
|
||||
"mount_execute_reload": "Reload",
|
||||
"mount_execute_remove": "Remove"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not apply the fix. Check the supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_system_multiple_data_disks": {
|
||||
"title": "Multiple data disks detected",
|
||||
"fix_flow": {
|
||||
|
@ -340,6 +340,28 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
|
||||
return attribute
|
||||
|
||||
|
||||
class BackupLocationSelectorConfig(TypedDict, total=False):
|
||||
"""Class to represent a backup location selector config."""
|
||||
|
||||
|
||||
@SELECTORS.register("backup_location")
|
||||
class BackupLocationSelector(Selector[BackupLocationSelectorConfig]):
|
||||
"""Selector of a backup location."""
|
||||
|
||||
selector_type = "backup_location"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({})
|
||||
|
||||
def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
|
||||
def __call__(self, data: Any) -> str:
|
||||
"""Validate the passed selection."""
|
||||
name: str = vol.Match(r"^(?:\/backup|\w+)$")(data)
|
||||
return name
|
||||
|
||||
|
||||
class BooleanSelectorConfig(TypedDict):
|
||||
"""Class to represent a boolean selector config."""
|
||||
|
||||
|
@ -571,6 +571,34 @@ async def test_service_calls(
|
||||
"password": "123456",
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
"hassio",
|
||||
"backup_full",
|
||||
{
|
||||
"location": "backup_share",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 17
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"location": "backup_share",
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
"hassio",
|
||||
"backup_full",
|
||||
{
|
||||
"location": "/backup",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count == 18
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"location": None,
|
||||
}
|
||||
|
||||
|
||||
async def test_service_calls_core(
|
||||
hassio_env, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
|
@ -400,3 +400,102 @@ async def test_supervisor_issue_repair_flow_skip_confirmation(
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1235"
|
||||
)
|
||||
|
||||
|
||||
async def test_mount_failed_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test repair flow for mount_failed issue."""
|
||||
issue_registry: ir.IssueRegistry = ir.async_get(hass)
|
||||
mock_resolution_info(
|
||||
aioclient_mock,
|
||||
issues=[
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "mount_failed",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": "1235",
|
||||
"type": "execute_reload",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
},
|
||||
{
|
||||
"uuid": "1236",
|
||||
"type": "execute_remove",
|
||||
"context": "mount",
|
||||
"reference": "backup_share",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
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": "menu",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"step_id": "fix_menu",
|
||||
"data_schema": [
|
||||
{
|
||||
"type": "select",
|
||||
"options": [
|
||||
["mount_execute_reload", "mount_execute_reload"],
|
||||
["mount_execute_remove", "mount_execute_remove"],
|
||||
],
|
||||
"name": "next_step_id",
|
||||
}
|
||||
],
|
||||
"menu_options": ["mount_execute_reload", "mount_execute_remove"],
|
||||
"description_placeholders": {
|
||||
"reference": "backup_share",
|
||||
"storage_url": "/config/storage",
|
||||
},
|
||||
}
|
||||
|
||||
resp = await client.post(
|
||||
f"/api/repairs/issues/fix/{flow_id}",
|
||||
json={"next_step_id": "mount_execute_reload"},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"version": 1,
|
||||
"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"
|
||||
)
|
||||
|
@ -415,6 +415,17 @@ def test_addon_selector_schema(schema, valid_selections, invalid_selections) ->
|
||||
_test_selector("addon", schema, valid_selections, invalid_selections)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "valid_selections", "invalid_selections"),
|
||||
(({}, ("abc123", "/backup"), (None, "abc@123", "abc 123", "")),),
|
||||
)
|
||||
def test_backup_location_selector_schema(
|
||||
schema, valid_selections, invalid_selections
|
||||
) -> None:
|
||||
"""Test backup location selector."""
|
||||
_test_selector("backup_location", schema, valid_selections, invalid_selections)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("schema", "valid_selections", "invalid_selections"),
|
||||
(({}, (1, "one", None), ()),), # Everything can be coerced to bool
|
||||
|
Loading…
x
Reference in New Issue
Block a user