Clean up backups after manual backup (#133434)

* Clean up backups after manual backup

* Address review comments
This commit is contained in:
Erik Montnemery 2024-12-17 20:00:02 +01:00 committed by GitHub
parent af1222e97b
commit 633433709f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 289 additions and 22 deletions

View File

@ -323,25 +323,6 @@ class BackupSchedule:
# and handled in the future # and handled in the future
LOGGER.exception("Unexpected error creating automatic backup") LOGGER.exception("Unexpected error creating automatic backup")
# delete old backups more numerous than copies
def _backups_filter(
backups: dict[str, ManagerBackup],
) -> dict[str, ManagerBackup]:
"""Return oldest backups more numerous than copies to delete."""
# we need to check here since we await before
# this filter is applied
if config_data.retention.copies is None:
return {}
return dict(
sorted(
backups.items(),
key=lambda backup_item: backup_item[1].date,
)[: len(backups) - config_data.retention.copies]
)
await _delete_filtered_backups(manager, _backups_filter)
manager.remove_next_backup_event = async_track_point_in_time( manager.remove_next_backup_event = async_track_point_in_time(
manager.hass, _create_backup, next_time manager.hass, _create_backup, next_time
) )
@ -469,3 +450,24 @@ async def _delete_filtered_backups(
"Error deleting old copies: %s", "Error deleting old copies: %s",
agent_errors, agent_errors,
) )
async def delete_backups_exceeding_configured_count(manager: BackupManager) -> None:
"""Delete backups exceeding the configured retention count."""
def _backups_filter(
backups: dict[str, ManagerBackup],
) -> dict[str, ManagerBackup]:
"""Return oldest backups more numerous than copies to delete."""
# we need to check here since we await before
# this filter is applied
if manager.config.data.retention.copies is None:
return {}
return dict(
sorted(
backups.items(),
key=lambda backup_item: backup_item[1].date,
)[: len(backups) - manager.config.data.retention.copies]
)
await _delete_filtered_backups(manager, _backups_filter)

View File

@ -33,7 +33,7 @@ from .agent import (
BackupAgentPlatformProtocol, BackupAgentPlatformProtocol,
LocalBackupAgent, LocalBackupAgent,
) )
from .config import BackupConfig from .config import BackupConfig, delete_backups_exceeding_configured_count
from .const import ( from .const import (
BUF_SIZE, BUF_SIZE,
DATA_MANAGER, DATA_MANAGER,
@ -750,6 +750,10 @@ class BackupManager:
self.known_backups.add( self.known_backups.add(
written_backup.backup, agent_errors, with_strategy_settings written_backup.backup, agent_errors, with_strategy_settings
) )
# delete old backups more numerous than copies
await delete_backups_exceeding_configured_count(self)
self.async_on_backup_event( self.async_on_backup_event(
CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED) CreateBackupEvent(stage=None, state=CreateBackupState.COMPLETED)
) )

View File

@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest import pytest
from homeassistant.components.backup.manager import WrittenBackup from homeassistant.components.backup.manager import NewBackup, WrittenBackup
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .common import TEST_BACKUP_PATH_ABC123 from .common import TEST_BACKUP_PATH_ABC123
@ -76,7 +76,7 @@ def mock_create_backup() -> Generator[AsyncMock]:
with patch( with patch(
"homeassistant.components.backup.CoreBackupReaderWriter.async_create_backup" "homeassistant.components.backup.CoreBackupReaderWriter.async_create_backup"
) as mock_create_backup: ) as mock_create_backup:
mock_create_backup.return_value = (MagicMock(), fut) mock_create_backup.return_value = (NewBackup(backup_job_id="abc123"), fut)
yield mock_create_backup yield mock_create_backup

View File

@ -1637,6 +1637,267 @@ async def test_config_retention_copies_logic(
) )
@pytest.mark.parametrize(
("backup_command", "backup_time"),
[
(
{"type": "backup/generate_with_strategy_settings"},
"2024-11-11T12:00:00+01:00",
),
(
{"type": "backup/generate", "agent_ids": ["test.test-agent"]},
None,
),
],
)
@pytest.mark.parametrize(
(
"config_command",
"backups",
"get_backups_agent_errors",
"delete_backup_agent_errors",
"backup_calls",
"get_backups_calls",
"delete_calls",
"delete_args_list",
),
[
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": None, "days": None},
"schedule": "never",
},
{
"backup-1": MagicMock(
date="2024-11-10T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-2": MagicMock(
date="2024-11-11T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-3": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-4": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=False,
spec=ManagerBackup,
),
},
{},
{},
1,
1, # we get backups even if backup retention copies is None
0,
[],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None},
"schedule": "never",
},
{
"backup-1": MagicMock(
date="2024-11-10T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-2": MagicMock(
date="2024-11-11T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-3": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-4": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=False,
spec=ManagerBackup,
),
},
{},
{},
1,
1,
0,
[],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 3, "days": None},
"schedule": "never",
},
{
"backup-1": MagicMock(
date="2024-11-09T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-2": MagicMock(
date="2024-11-10T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-3": MagicMock(
date="2024-11-11T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-4": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-5": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=False,
spec=ManagerBackup,
),
},
{},
{},
1,
1,
1,
[call("backup-1")],
),
(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 2, "days": None},
"schedule": "never",
},
{
"backup-1": MagicMock(
date="2024-11-09T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-2": MagicMock(
date="2024-11-10T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-3": MagicMock(
date="2024-11-11T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-4": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=True,
spec=ManagerBackup,
),
"backup-5": MagicMock(
date="2024-11-12T04:45:00+01:00",
with_strategy_settings=False,
spec=ManagerBackup,
),
},
{},
{},
1,
1,
2,
[call("backup-1"), call("backup-2")],
),
],
)
async def test_config_retention_copies_logic_manual_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
hass_storage: dict[str, Any],
create_backup: AsyncMock,
delete_backup: AsyncMock,
get_backups: AsyncMock,
config_command: dict[str, Any],
backup_command: dict[str, Any],
backups: dict[str, Any],
get_backups_agent_errors: dict[str, Exception],
delete_backup_agent_errors: dict[str, Exception],
backup_time: str,
backup_calls: int,
get_backups_calls: int,
delete_calls: int,
delete_args_list: Any,
) -> None:
"""Test config backup retention copies logic for manual backup."""
client = await hass_ws_client(hass)
storage_data = {
"backups": {},
"config": {
"create_backup": {
"agent_ids": ["test-agent"],
"include_addons": ["test-addon"],
"include_all_addons": False,
"include_database": True,
"include_folders": ["media"],
"name": "test-name",
"password": "test-password",
},
"retention": {"copies": None, "days": None},
"last_attempted_strategy_backup": None,
"last_completed_strategy_backup": None,
"schedule": {"state": "daily"},
},
}
hass_storage[DOMAIN] = {
"data": storage_data,
"key": DOMAIN,
"version": 1,
}
get_backups.return_value = (backups, get_backups_agent_errors)
delete_backup.return_value = delete_backup_agent_errors
await hass.config.async_set_time_zone("Europe/Amsterdam")
freezer.move_to("2024-11-11 12:00:00+01:00")
await setup_backup_integration(hass, remote_agents=["test-agent"])
await hass.async_block_till_done()
await client.send_json_auto_id(config_command)
result = await client.receive_json()
assert result["success"]
# Create a manual backup
await client.send_json_auto_id(backup_command)
result = await client.receive_json()
assert result["success"]
# Wait for backup creation to complete
await hass.async_block_till_done()
assert create_backup.call_count == backup_calls
assert get_backups.call_count == get_backups_calls
assert delete_backup.call_count == delete_calls
assert delete_backup.call_args_list == delete_args_list
async_fire_time_changed(hass, fire_all=True) # flush out storage save
await hass.async_block_till_done()
assert (
hass_storage[DOMAIN]["data"]["config"]["last_attempted_strategy_backup"]
== backup_time
)
assert (
hass_storage[DOMAIN]["data"]["config"]["last_completed_strategy_backup"]
== backup_time
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
"command", "command",