From 35bcf826273c812a730ac2427418dc59f11c4d9d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Feb 2025 16:24:30 +0100 Subject: [PATCH] Correct invalid automatic backup settings when loading from store (#138716) * Correct invalid automatic backup settings when loading from store * Improve docstring * Improve tests --- homeassistant/components/backup/__init__.py | 2 + homeassistant/components/backup/manager.py | 49 +- homeassistant/components/hassio/backup.py | 4 + .../backup/snapshots/test_websocket.ambr | 494 +++++++++++++++++- tests/components/backup/test_websocket.py | 85 ++- 5 files changed, 618 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py index 71a4f5ea41a..1b19b185b4f 100644 --- a/homeassistant/components/backup/__init__.py +++ b/homeassistant/components/backup/__init__.py @@ -16,6 +16,7 @@ from .agent import ( BackupAgentPlatformProtocol, LocalBackupAgent, ) +from .config import BackupConfig from .const import DATA_MANAGER, DOMAIN from .http import async_register_http_views from .manager import ( @@ -47,6 +48,7 @@ __all__ = [ "BackupAgent", "BackupAgentError", "BackupAgentPlatformProtocol", + "BackupConfig", "BackupManagerError", "BackupNotFound", "BackupPlatformProtocol", diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 81826ffcb24..5a1bcde2b3b 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -43,7 +43,11 @@ from .agent import ( BackupAgentPlatformProtocol, LocalBackupAgent, ) -from .config import BackupConfig, delete_backups_exceeding_configured_count +from .config import ( + BackupConfig, + CreateBackupParametersDict, + delete_backups_exceeding_configured_count, +) from .const import ( BUF_SIZE, DATA_MANAGER, @@ -282,6 +286,10 @@ class BackupReaderWriter(abc.ABC): ) -> None: """Get restore events after core restart.""" + @abc.abstractmethod + async def async_validate_config(self, *, config: BackupConfig) -> None: + """Validate backup config.""" + class IncorrectPasswordError(BackupReaderWriterError): """Raised when the password is incorrect.""" @@ -333,6 +341,7 @@ class BackupManager: self.config.load(stored["config"]) self.known_backups.load(stored["backups"]) + await self._reader_writer.async_validate_config(config=self.config) await self._reader_writer.async_resume_restore_progress_after_restart( on_progress=self.async_on_backup_event ) @@ -1832,6 +1841,44 @@ class CoreBackupReaderWriter(BackupReaderWriter): ) on_progress(IdleEvent()) + async def async_validate_config(self, *, config: BackupConfig) -> None: + """Validate backup config. + + Update automatic backup settings to not include addons or folders and remove + hassio agents in case a backup created by supervisor was restored. + """ + create_backup = config.data.create_backup + if ( + not create_backup.include_addons + and not create_backup.include_all_addons + and not create_backup.include_folders + and not any(a_id.startswith("hassio.") for a_id in create_backup.agent_ids) + ): + LOGGER.debug("Backup settings don't need to be adjusted") + return + + LOGGER.info( + "Adjusting backup settings to not include addons, folders or supervisor locations" + ) + automatic_agents = [ + agent_id + for agent_id in create_backup.agent_ids + if not agent_id.startswith("hassio.") + ] + if ( + self._local_agent_id not in automatic_agents + and "hassio.local" in create_backup.agent_ids + ): + automatic_agents = [self._local_agent_id, *automatic_agents] + await config.update( + create_backup=CreateBackupParametersDict( + agent_ids=automatic_agents, + include_addons=None, + include_all_addons=False, + include_folders=None, + ) + ) + def _generate_backup_id(date: str, name: str) -> str: """Generate a backup ID.""" diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index ddaa821587f..9c0511a93fe 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -27,6 +27,7 @@ from homeassistant.components.backup import ( AddonInfo, AgentBackup, BackupAgent, + BackupConfig, BackupManagerError, BackupNotFound, BackupReaderWriter, @@ -633,6 +634,9 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): _LOGGER.debug("Could not get restore job %s: %s", restore_job_id, err) unsub() + async def async_validate_config(self, *, config: BackupConfig) -> None: + """Validate backup config.""" + @callback def _async_listen_job_events( self, job_id: UUID, on_event: Callable[[Mapping[str, Any]], None] diff --git a/tests/components/backup/snapshots/test_websocket.ambr b/tests/components/backup/snapshots/test_websocket.ambr index 2f063262f34..572ed9b06fa 100644 --- a/tests/components/backup/snapshots/test_websocket.ambr +++ b/tests/components/backup/snapshots/test_websocket.ambr @@ -251,7 +251,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data0] +# name: test_config_load_config_info[with_hassio-storage_data0] dict({ 'id': 1, 'result': dict({ @@ -288,7 +288,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data1] +# name: test_config_load_config_info[with_hassio-storage_data1] dict({ 'id': 1, 'result': dict({ @@ -337,7 +337,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data2] +# name: test_config_load_config_info[with_hassio-storage_data2] dict({ 'id': 1, 'result': dict({ @@ -375,7 +375,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data3] +# name: test_config_load_config_info[with_hassio-storage_data3] dict({ 'id': 1, 'result': dict({ @@ -413,7 +413,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data4] +# name: test_config_load_config_info[with_hassio-storage_data4] dict({ 'id': 1, 'result': dict({ @@ -452,7 +452,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data5] +# name: test_config_load_config_info[with_hassio-storage_data5] dict({ 'id': 1, 'result': dict({ @@ -490,7 +490,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data6] +# name: test_config_load_config_info[with_hassio-storage_data6] dict({ 'id': 1, 'result': dict({ @@ -530,7 +530,7 @@ 'type': 'result', }) # --- -# name: test_config_info[storage_data7] +# name: test_config_load_config_info[with_hassio-storage_data7] dict({ 'id': 1, 'result': dict({ @@ -576,6 +576,484 @@ 'type': 'result', }) # --- +# name: test_config_load_config_info[with_hassio-storage_data8] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'hassio.local', + 'hassio.share', + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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_load_config_info[with_hassio-storage_data9] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'backup.local', + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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_load_config_info[without_hassio-storage_data0] + 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_load_config_info[without_hassio-storage_data1] + dict({ + 'id': 1, + 'result': dict({ + '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': 'test-name', + 'password': 'test-password', + }), + 'last_attempted_automatic_backup': '2024-10-26T04:45:00+01:00', + 'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00', + 'next_automatic_backup': '2024-11-14T04:55:00+01:00', + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': 3, + 'days': 7, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + 'tue', + 'wed', + 'thu', + 'fri', + 'sat', + 'sun', + ]), + 'recurrence': 'custom_days', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data2] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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': 3, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data3] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': '2024-10-27T04:45:00+01:00', + 'last_completed_automatic_backup': '2024-10-26T04:45:00+01:00', + 'next_automatic_backup': None, + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': 7, + }), + 'schedule': dict({ + 'days': list([ + ]), + 'recurrence': 'never', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data4] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-18T04:55:00+01:00', + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + ]), + 'recurrence': 'custom_days', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data5] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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_load_config_info[without_hassio-storage_data6] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-17T04:55:00+01:00', + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data7] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + 'test-agent1': dict({ + 'protected': True, + }), + 'test-agent2': dict({ + 'protected': False, + }), + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + 'include_folders': None, + 'name': None, + 'password': None, + }), + 'last_attempted_automatic_backup': None, + 'last_completed_automatic_backup': None, + 'next_automatic_backup': '2024-11-17T04:55:00+01:00', + 'next_automatic_backup_additional': False, + 'retention': dict({ + 'copies': None, + 'days': None, + }), + 'schedule': dict({ + 'days': list([ + 'mon', + 'sun', + ]), + 'recurrence': 'custom_days', + 'time': None, + }), + }), + }), + 'success': True, + 'type': 'result', + }) +# --- +# name: test_config_load_config_info[without_hassio-storage_data8] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'backup.local', + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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_load_config_info[without_hassio-storage_data9] + dict({ + 'id': 1, + 'result': dict({ + 'config': dict({ + 'agents': dict({ + }), + 'create_backup': dict({ + 'agent_ids': list([ + 'backup.local', + 'test-agent', + ]), + 'include_addons': None, + 'include_all_addons': False, + 'include_database': False, + '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[commands0] dict({ 'id': 1, diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 773256bdd0b..82d2c0a921d 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -46,10 +46,10 @@ BACKUP_CALL = call( agent_ids=["test.test-agent"], backup_name="test-name", extra_metadata={"instance_id": ANY, "with_automatic_settings": True}, - include_addons=["test-addon"], + include_addons=[], include_all_addons=False, include_database=True, - include_folders=["media"], + include_folders=None, include_homeassistant=True, password="test-password", on_progress=ANY, @@ -1126,25 +1126,96 @@ async def test_agents_info( "minor_version": store.STORAGE_VERSION_MINOR, }, }, + { + "backup": { + "data": { + "backups": [], + "config": { + "agents": {}, + "create_backup": { + "agent_ids": ["hassio.local", "hassio.share", "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": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "version": store.STORAGE_VERSION, + "minor_version": store.STORAGE_VERSION_MINOR, + }, + }, + { + "backup": { + "data": { + "backups": [], + "config": { + "agents": {}, + "create_backup": { + "agent_ids": ["backup.local", "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": [], + "recurrence": "never", + "state": "never", + "time": None, + }, + }, + }, + "key": DOMAIN, + "version": store.STORAGE_VERSION, + "minor_version": store.STORAGE_VERSION_MINOR, + }, + }, ], ) +@pytest.mark.parametrize( + ("with_hassio"), + [ + pytest.param(True, id="with_hassio"), + pytest.param(False, id="without_hassio"), + ], +) +@pytest.mark.usefixtures("supervisor_client") @patch("homeassistant.components.backup.config.random.randint", Mock(return_value=600)) -async def test_config_info( +async def test_config_load_config_info( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion, hass_storage: dict[str, Any], + with_hassio: bool, storage_data: dict[str, Any] | None, ) -> None: - """Test getting backup config info.""" + """Test loading stored backup config and reading it via config/info.""" client = await hass_ws_client(hass) await hass.config.async_set_time_zone("Europe/Amsterdam") freezer.move_to("2024-11-13T12:01:00+01:00") hass_storage.update(storage_data) - await setup_backup_integration(hass) + await setup_backup_integration(hass, with_hassio=with_hassio) await hass.async_block_till_done() await client.send_json_auto_id({"type": "backup/config/info"}) @@ -1702,10 +1773,10 @@ async def test_config_schedule_logic( "agents": {}, "create_backup": { "agent_ids": ["test.test-agent"], - "include_addons": ["test-addon"], + "include_addons": [], "include_all_addons": False, "include_database": True, - "include_folders": ["media"], + "include_folders": [], "name": "test-name", "password": "test-password", },