Don't allow setting backup retention to 0 days or copies (#138771)

* Don't allow setting backup retention to 0 days or copies

* Add tests
This commit is contained in:
Erik Montnemery 2025-02-18 15:16:44 +01:00 committed by Franck Nijhof
parent ac21d2855c
commit 59651c6f10
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
6 changed files with 312 additions and 26 deletions

View File

@ -16,7 +16,7 @@ if TYPE_CHECKING:
STORE_DELAY_SAVE = 30
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_VERSION_MINOR = 3
STORAGE_VERSION_MINOR = 4
class StoredBackupData(TypedDict):
@ -60,6 +60,13 @@ class _BackupStore(Store[StoredBackupData]):
else:
data["config"]["schedule"]["days"] = [state]
data["config"]["schedule"]["recurrence"] = "custom_days"
if old_minor_version < 4:
# Workaround for a bug in frontend which incorrectly set days to 0
# instead of to None for unlimited retention.
if data["config"]["retention"]["copies"] == 0:
data["config"]["retention"]["copies"] = None
if data["config"]["retention"]["days"] == 0:
data["config"]["retention"]["days"] = None
# Note: We allow reading data with major version 2.
# Reject if major version is higher than 2.

View File

@ -368,8 +368,10 @@ async def handle_config_info(
),
vol.Optional("retention"): vol.Schema(
{
vol.Optional("copies"): vol.Any(int, None),
vol.Optional("days"): vol.Any(int, None),
# Note: We can't use cv.positive_int because it allows 0 even
# though 0 is not positive.
vol.Optional("copies"): vol.Any(vol.All(int, vol.Range(min=1)), None),
vol.Optional("days"): vol.Any(vol.All(int, vol.Range(min=1)), None),
},
),
vol.Optional("schedule"): vol.Schema(

View File

@ -39,7 +39,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -84,11 +84,100 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
# name: test_store_migration[store_data1]
dict({
'data': dict({
'backups': list([
dict({
'backup_id': 'abc123',
'failed_agent_ids': list([
'test.remote',
]),
}),
]),
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'state': 'never',
'time': None,
}),
}),
}),
'key': 'backup',
'minor_version': 4,
'version': 1,
})
# ---
# name: test_store_migration[store_data1].1
dict({
'data': dict({
'backups': list([
dict({
'backup_id': 'abc123',
'failed_agent_ids': list([
'test.remote',
]),
}),
]),
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
'test-agent',
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'state': 'never',
'time': None,
}),
}),
}),
'key': 'backup',
'minor_version': 4,
'version': 1,
})
# ---
# name: test_store_migration[store_data2]
dict({
'data': dict({
'backups': list([
@ -131,11 +220,11 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
# name: test_store_migration[store_data1].1
# name: test_store_migration[store_data2].1
dict({
'data': dict({
'backups': list([
@ -179,7 +268,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---

View File

@ -1164,7 +1164,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1278,7 +1278,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1392,7 +1392,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1516,7 +1516,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1683,7 +1683,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1797,7 +1797,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -1913,7 +1913,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2027,7 +2027,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2145,7 +2145,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2267,7 +2267,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2381,7 +2381,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2495,7 +2495,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2609,7 +2609,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2723,7 +2723,7 @@
}),
}),
'key': 'backup',
'minor_version': 3,
'minor_version': 4,
'version': 1,
})
# ---
@ -2801,6 +2801,154 @@
'type': 'result',
})
# ---
# name: test_config_update_errors[command10]
dict({
'id': 1,
'result': dict({
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'next_automatic_backup_additional': False,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command10].1
dict({
'id': 3,
'result': dict({
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'next_automatic_backup_additional': False,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command11]
dict({
'id': 1,
'result': dict({
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'next_automatic_backup_additional': False,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command11].1
dict({
'id': 3,
'result': dict({
'config': dict({
'agents': dict({
}),
'create_backup': dict({
'agent_ids': list([
]),
'include_addons': None,
'include_all_addons': False,
'include_database': True,
'include_folders': None,
'name': None,
'password': None,
}),
'last_attempted_automatic_backup': None,
'last_completed_automatic_backup': None,
'next_automatic_backup': None,
'next_automatic_backup_additional': False,
'retention': dict({
'copies': None,
'days': None,
}),
'schedule': dict({
'days': list([
]),
'recurrence': 'never',
'time': None,
}),
}),
}),
'success': True,
'type': 'result',
})
# ---
# name: test_config_update_errors[command1]
dict({
'id': 1,

View File

@ -57,6 +57,38 @@ def mock_delay_save() -> Generator[None]:
"key": DOMAIN,
"version": 1,
},
{
"data": {
"backups": [
{
"backup_id": "abc123",
"failed_agent_ids": ["test.remote"],
}
],
"config": {
"create_backup": {
"agent_ids": [],
"include_addons": None,
"include_all_addons": False,
"include_database": True,
"include_folders": None,
"name": None,
"password": None,
},
"last_attempted_automatic_backup": None,
"last_completed_automatic_backup": None,
"retention": {
"copies": 0,
"days": 0,
},
"schedule": {
"state": "never",
},
},
},
"key": DOMAIN,
"version": 1,
},
{
"data": {
"backups": [

View File

@ -1437,6 +1437,14 @@ async def test_config_update(
"type": "backup/config/update",
"agents": {"test-agent1": {"favorite": True}},
},
{
"type": "backup/config/update",
"retention": {"copies": 0},
},
{
"type": "backup/config/update",
"retention": {"days": 0},
},
],
)
async def test_config_update_errors(
@ -2234,7 +2242,7 @@ async def test_config_schedule_logic(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None},
"retention": {"copies": 1, "days": None},
"schedule": {"recurrence": "daily"},
},
{
@ -2308,7 +2316,7 @@ async def test_config_schedule_logic(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None},
"retention": {"copies": 1, "days": None},
"schedule": {"recurrence": "daily"},
},
{
@ -2377,7 +2385,7 @@ async def test_config_schedule_logic(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test.test-agent"]},
"retention": {"copies": 0, "days": None},
"retention": {"copies": 1, "days": None},
"schedule": {"recurrence": "daily"},
},
{
@ -3098,7 +3106,7 @@ async def test_config_retention_copies_logic_manual_backup(
{
"type": "backup/config/update",
"create_backup": {"agent_ids": ["test-agent"]},
"retention": {"copies": None, "days": 0},
"retention": {"copies": None, "days": 1},
"schedule": {"recurrence": "never"},
}
],