mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 02:37:08 +00:00
Create repair for configured unavailable backup agents (#137382)
* Create repair for configured not loaded agents * Rework to repair issue * Extract logic to config function * Update test * Handle empty agend ids config update * Address review comment * Update tests * Address comment
This commit is contained in:
parent
f607b95c00
commit
27f7085b61
@ -12,16 +12,19 @@ from typing import TYPE_CHECKING, Self, TypedDict
|
|||||||
from cronsim import CronSim
|
from cronsim import CronSim
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
|
from homeassistant.helpers.event import async_call_later, async_track_point_in_time
|
||||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
from .models import BackupManagerError, Folder
|
from .models import BackupManagerError, Folder
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .manager import BackupManager, ManagerBackup
|
from .manager import BackupManager, ManagerBackup
|
||||||
|
|
||||||
|
AUTOMATIC_BACKUP_AGENTS_UNAVAILABLE_ISSUE_ID = "automatic_backup_agents_unavailable"
|
||||||
|
|
||||||
CRON_PATTERN_DAILY = "{m} {h} * * *"
|
CRON_PATTERN_DAILY = "{m} {h} * * *"
|
||||||
CRON_PATTERN_WEEKLY = "{m} {h} * * {d}"
|
CRON_PATTERN_WEEKLY = "{m} {h} * * {d}"
|
||||||
|
|
||||||
@ -151,6 +154,7 @@ class BackupConfig:
|
|||||||
retention=RetentionConfig(),
|
retention=RetentionConfig(),
|
||||||
schedule=BackupSchedule(),
|
schedule=BackupSchedule(),
|
||||||
)
|
)
|
||||||
|
self._hass = hass
|
||||||
self._manager = manager
|
self._manager = manager
|
||||||
|
|
||||||
def load(self, stored_config: StoredBackupConfig) -> None:
|
def load(self, stored_config: StoredBackupConfig) -> None:
|
||||||
@ -182,6 +186,8 @@ class BackupConfig:
|
|||||||
self.data.automatic_backups_configured = automatic_backups_configured
|
self.data.automatic_backups_configured = automatic_backups_configured
|
||||||
if create_backup is not UNDEFINED:
|
if create_backup is not UNDEFINED:
|
||||||
self.data.create_backup = replace(self.data.create_backup, **create_backup)
|
self.data.create_backup = replace(self.data.create_backup, **create_backup)
|
||||||
|
if "agent_ids" in create_backup:
|
||||||
|
check_unavailable_agents(self._hass, self._manager)
|
||||||
if retention is not UNDEFINED:
|
if retention is not UNDEFINED:
|
||||||
new_retention = RetentionConfig(**retention)
|
new_retention = RetentionConfig(**retention)
|
||||||
if new_retention != self.data.retention:
|
if new_retention != self.data.retention:
|
||||||
@ -562,3 +568,46 @@ async def delete_backups_exceeding_configured_count(manager: BackupManager) -> N
|
|||||||
await manager.async_delete_filtered_backups(
|
await manager.async_delete_filtered_backups(
|
||||||
include_filter=_automatic_backups_filter, delete_filter=_delete_filter
|
include_filter=_automatic_backups_filter, delete_filter=_delete_filter
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def check_unavailable_agents(hass: HomeAssistant, manager: BackupManager) -> None:
|
||||||
|
"""Check for unavailable agents."""
|
||||||
|
if missing_agent_ids := set(manager.config.data.create_backup.agent_ids) - set(
|
||||||
|
manager.backup_agents
|
||||||
|
):
|
||||||
|
LOGGER.debug(
|
||||||
|
"Agents %s are configured for automatic backup but are unavailable",
|
||||||
|
missing_agent_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove issues for unavailable agents that are not unavailable anymore.
|
||||||
|
issue_registry = ir.async_get(hass)
|
||||||
|
existing_missing_agent_issue_ids = {
|
||||||
|
issue_id
|
||||||
|
for domain, issue_id in issue_registry.issues
|
||||||
|
if domain == DOMAIN
|
||||||
|
and issue_id.startswith(AUTOMATIC_BACKUP_AGENTS_UNAVAILABLE_ISSUE_ID)
|
||||||
|
}
|
||||||
|
current_missing_agent_issue_ids = {
|
||||||
|
f"{AUTOMATIC_BACKUP_AGENTS_UNAVAILABLE_ISSUE_ID}_{agent_id}": agent_id
|
||||||
|
for agent_id in missing_agent_ids
|
||||||
|
}
|
||||||
|
for issue_id in existing_missing_agent_issue_ids - set(
|
||||||
|
current_missing_agent_issue_ids
|
||||||
|
):
|
||||||
|
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||||
|
for issue_id, agent_id in current_missing_agent_issue_ids.items():
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
issue_id,
|
||||||
|
is_fixable=False,
|
||||||
|
learn_more_url="homeassistant://config/backup",
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key="automatic_backup_agents_unavailable",
|
||||||
|
translation_placeholders={
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"backup_settings": "/config/backup/settings",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -32,6 +32,7 @@ from homeassistant.helpers import (
|
|||||||
instance_id,
|
instance_id,
|
||||||
integration_platform,
|
integration_platform,
|
||||||
issue_registry as ir,
|
issue_registry as ir,
|
||||||
|
start,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.backup import DATA_BACKUP
|
from homeassistant.helpers.backup import DATA_BACKUP
|
||||||
from homeassistant.helpers.json import json_bytes
|
from homeassistant.helpers.json import json_bytes
|
||||||
@ -47,6 +48,7 @@ from .agent import (
|
|||||||
from .config import (
|
from .config import (
|
||||||
BackupConfig,
|
BackupConfig,
|
||||||
CreateBackupParametersDict,
|
CreateBackupParametersDict,
|
||||||
|
check_unavailable_agents,
|
||||||
delete_backups_exceeding_configured_count,
|
delete_backups_exceeding_configured_count,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -417,6 +419,13 @@ class BackupManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def check_unavailable_agents_after_start(hass: HomeAssistant) -> None:
|
||||||
|
"""Check unavailable agents after start."""
|
||||||
|
check_unavailable_agents(hass, self)
|
||||||
|
|
||||||
|
start.async_at_started(self.hass, check_unavailable_agents_after_start)
|
||||||
|
|
||||||
async def _add_platform(
|
async def _add_platform(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"issues": {
|
"issues": {
|
||||||
|
"automatic_backup_agents_unavailable": {
|
||||||
|
"title": "The backup location {agent_id} is unavailable",
|
||||||
|
"description": "The backup location `{agent_id}` is unavailable but is still configured for automatic backups.\n\nPlease visit the [automatic backup configuration page]({backup_settings}) to review and update your backup locations. Backups will not be uploaded to selected locations that are unavailable."
|
||||||
|
},
|
||||||
"automatic_backup_failed_create": {
|
"automatic_backup_failed_create": {
|
||||||
"title": "Automatic backup could not be created",
|
"title": "Automatic backup could not be created",
|
||||||
"description": "The automatic backup could not be created. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
|
"description": "The automatic backup could not be created. Please check the logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured."
|
||||||
|
@ -982,7 +982,15 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]:
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
True,
|
True,
|
||||||
{},
|
{
|
||||||
|
(DOMAIN, "automatic_backup_agents_unavailable_test.unknown"): {
|
||||||
|
"translation_key": "automatic_backup_agents_unavailable",
|
||||||
|
"translation_placeholders": {
|
||||||
|
"agent_id": "test.unknown",
|
||||||
|
"backup_settings": "/config/backup/settings",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
["test.remote", "test.unknown"],
|
["test.remote", "test.unknown"],
|
||||||
@ -994,7 +1002,14 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]:
|
|||||||
(DOMAIN, "automatic_backup_failed"): {
|
(DOMAIN, "automatic_backup_failed"): {
|
||||||
"translation_key": "automatic_backup_failed_upload_agents",
|
"translation_key": "automatic_backup_failed_upload_agents",
|
||||||
"translation_placeholders": {"failed_agents": "test.unknown"},
|
"translation_placeholders": {"failed_agents": "test.unknown"},
|
||||||
}
|
},
|
||||||
|
(DOMAIN, "automatic_backup_agents_unavailable_test.unknown"): {
|
||||||
|
"translation_key": "automatic_backup_agents_unavailable",
|
||||||
|
"translation_placeholders": {
|
||||||
|
"agent_id": "test.unknown",
|
||||||
|
"backup_settings": "/config/backup/settings",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# Error raised in async_initiate_backup
|
# Error raised in async_initiate_backup
|
||||||
|
@ -27,6 +27,7 @@ from homeassistant.components.backup.manager import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import issue_registry as ir
|
||||||
from homeassistant.helpers.backup import async_initialize_backup
|
from homeassistant.helpers.backup import async_initialize_backup
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
@ -34,7 +35,9 @@ from .common import (
|
|||||||
LOCAL_AGENT_ID,
|
LOCAL_AGENT_ID,
|
||||||
TEST_BACKUP_ABC123,
|
TEST_BACKUP_ABC123,
|
||||||
TEST_BACKUP_DEF456,
|
TEST_BACKUP_DEF456,
|
||||||
|
mock_backup_agent,
|
||||||
setup_backup_integration,
|
setup_backup_integration,
|
||||||
|
setup_backup_platform,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed, async_mock_service
|
from tests.common import async_fire_time_changed, async_mock_service
|
||||||
@ -3244,6 +3247,185 @@ async def test_config_retention_days_logic(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_configured_agents_unavailable_repair(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
issue_registry: ir.IssueRegistry,
|
||||||
|
hass_storage: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test creating and deleting repair issue for configured unavailable agents."""
|
||||||
|
issue_id = "automatic_backup_agents_unavailable_test.agent"
|
||||||
|
ws_client = await hass_ws_client(hass)
|
||||||
|
hass_storage.update(
|
||||||
|
{
|
||||||
|
"backup": {
|
||||||
|
"data": {
|
||||||
|
"backups": [],
|
||||||
|
"config": {
|
||||||
|
"agents": {},
|
||||||
|
"automatic_backups_configured": True,
|
||||||
|
"create_backup": {
|
||||||
|
"agent_ids": ["test.agent"],
|
||||||
|
"include_addons": None,
|
||||||
|
"include_all_addons": False,
|
||||||
|
"include_database": False,
|
||||||
|
"include_folders": None,
|
||||||
|
"name": None,
|
||||||
|
"password": None,
|
||||||
|
},
|
||||||
|
"retention": {"copies": None, "days": None},
|
||||||
|
"last_attempted_automatic_backup": None,
|
||||||
|
"last_completed_automatic_backup": None,
|
||||||
|
"schedule": {
|
||||||
|
"days": ["mon"],
|
||||||
|
"recurrence": "custom_days",
|
||||||
|
"state": "never",
|
||||||
|
"time": None,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"key": DOMAIN,
|
||||||
|
"version": store.STORAGE_VERSION,
|
||||||
|
"minor_version": store.STORAGE_VERSION_MINOR,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await setup_backup_integration(hass)
|
||||||
|
get_agents_mock = AsyncMock(return_value=[mock_backup_agent("agent")])
|
||||||
|
register_listener_mock = Mock()
|
||||||
|
await setup_backup_platform(
|
||||||
|
hass,
|
||||||
|
domain="test",
|
||||||
|
platform=Mock(
|
||||||
|
async_get_backup_agents=get_agents_mock,
|
||||||
|
async_register_backup_agents_listener=register_listener_mock,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
reload_backup_agents = register_listener_mock.call_args[1]["listener"]
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/agents/info"})
|
||||||
|
resp = await ws_client.receive_json()
|
||||||
|
assert resp["result"]["agents"] == [
|
||||||
|
{"agent_id": "backup.local", "name": "local"},
|
||||||
|
{"agent_id": "test.agent", "name": "agent"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
# Reload the agents with no agents returned.
|
||||||
|
|
||||||
|
get_agents_mock.return_value = []
|
||||||
|
reload_backup_agents()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/agents/info"})
|
||||||
|
resp = await ws_client.receive_json()
|
||||||
|
assert resp["result"]["agents"] == [
|
||||||
|
{"agent_id": "backup.local", "name": "local"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == ["test.agent"]
|
||||||
|
|
||||||
|
# Update the automatic backup configuration removing the unavailable agent.
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "backup/config/update",
|
||||||
|
"create_backup": {"agent_ids": ["backup.local"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == ["backup.local"]
|
||||||
|
|
||||||
|
# Reload the agents with one agent returned
|
||||||
|
# but not configured for automatic backups.
|
||||||
|
|
||||||
|
get_agents_mock.return_value = [mock_backup_agent("agent")]
|
||||||
|
reload_backup_agents()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/agents/info"})
|
||||||
|
resp = await ws_client.receive_json()
|
||||||
|
assert resp["result"]["agents"] == [
|
||||||
|
{"agent_id": "backup.local", "name": "local"},
|
||||||
|
{"agent_id": "test.agent", "name": "agent"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == ["backup.local"]
|
||||||
|
|
||||||
|
# Update the automatic backup configuration and configure the test agent.
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "backup/config/update",
|
||||||
|
"create_backup": {"agent_ids": ["backup.local", "test.agent"]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == [
|
||||||
|
"backup.local",
|
||||||
|
"test.agent",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Reload the agents with no agents returned again.
|
||||||
|
|
||||||
|
get_agents_mock.return_value = []
|
||||||
|
reload_backup_agents()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/agents/info"})
|
||||||
|
resp = await ws_client.receive_json()
|
||||||
|
assert resp["result"]["agents"] == [
|
||||||
|
{"agent_id": "backup.local", "name": "local"},
|
||||||
|
]
|
||||||
|
|
||||||
|
assert issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == [
|
||||||
|
"backup.local",
|
||||||
|
"test.agent",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Update the automatic backup configuration removing all agents.
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "backup/config/update",
|
||||||
|
"create_backup": {"agent_ids": []},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
|
||||||
|
assert not issue_registry.async_get_issue(domain=DOMAIN, issue_id=issue_id)
|
||||||
|
|
||||||
|
await ws_client.send_json_auto_id({"type": "backup/config/info"})
|
||||||
|
result = await ws_client.receive_json()
|
||||||
|
assert result["result"]["config"]["create_backup"]["agent_ids"] == []
|
||||||
|
|
||||||
|
|
||||||
async def test_subscribe_event(
|
async def test_subscribe_event(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user