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 commentpull/139272/head
parent
f607b95c00
commit
27f7085b61
|
@ -12,16 +12,19 @@ from typing import TYPE_CHECKING, Self, TypedDict
|
|||
from cronsim import CronSim
|
||||
|
||||
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.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import LOGGER
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .models import BackupManagerError, Folder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import BackupManager, ManagerBackup
|
||||
|
||||
AUTOMATIC_BACKUP_AGENTS_UNAVAILABLE_ISSUE_ID = "automatic_backup_agents_unavailable"
|
||||
|
||||
CRON_PATTERN_DAILY = "{m} {h} * * *"
|
||||
CRON_PATTERN_WEEKLY = "{m} {h} * * {d}"
|
||||
|
||||
|
@ -151,6 +154,7 @@ class BackupConfig:
|
|||
retention=RetentionConfig(),
|
||||
schedule=BackupSchedule(),
|
||||
)
|
||||
self._hass = hass
|
||||
self._manager = manager
|
||||
|
||||
def load(self, stored_config: StoredBackupConfig) -> None:
|
||||
|
@ -182,6 +186,8 @@ class BackupConfig:
|
|||
self.data.automatic_backups_configured = automatic_backups_configured
|
||||
if create_backup is not UNDEFINED:
|
||||
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:
|
||||
new_retention = RetentionConfig(**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(
|
||||
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,
|
||||
integration_platform,
|
||||
issue_registry as ir,
|
||||
start,
|
||||
)
|
||||
from homeassistant.helpers.backup import DATA_BACKUP
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
|
@ -47,6 +48,7 @@ from .agent import (
|
|||
from .config import (
|
||||
BackupConfig,
|
||||
CreateBackupParametersDict,
|
||||
check_unavailable_agents,
|
||||
delete_backups_exceeding_configured_count,
|
||||
)
|
||||
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(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"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": {
|
||||
"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."
|
||||
|
|
|
@ -982,7 +982,15 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]:
|
|||
None,
|
||||
None,
|
||||
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"],
|
||||
|
@ -994,7 +1002,14 @@ async def delayed_boom(*args, **kwargs) -> tuple[NewBackup, Any]:
|
|||
(DOMAIN, "automatic_backup_failed"): {
|
||||
"translation_key": "automatic_backup_failed_upload_agents",
|
||||
"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
|
||||
|
|
|
@ -27,6 +27,7 @@ from homeassistant.components.backup.manager import (
|
|||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.backup import async_initialize_backup
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
@ -34,7 +35,9 @@ from .common import (
|
|||
LOCAL_AGENT_ID,
|
||||
TEST_BACKUP_ABC123,
|
||||
TEST_BACKUP_DEF456,
|
||||
mock_backup_agent,
|
||||
setup_backup_integration,
|
||||
setup_backup_platform,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
|
|
Loading…
Reference in New Issue