mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Persist backup restore status after core restart (#136838)
* Persist backup restore status after core restart * Don't blow up if restore result file can't be removed * Update tests
This commit is contained in:
parent
8ab6bec746
commit
b2ec72d75f
@ -18,6 +18,7 @@ import securetar
|
||||
from .const import __version__ as HA_VERSION
|
||||
|
||||
RESTORE_BACKUP_FILE = ".HA_RESTORE"
|
||||
RESTORE_BACKUP_RESULT_FILE = ".HA_RESTORE_RESULT"
|
||||
KEEP_BACKUPS = ("backups",)
|
||||
KEEP_DATABASE = (
|
||||
"home-assistant_v2.db",
|
||||
@ -62,7 +63,10 @@ def restore_backup_file_content(config_dir: Path) -> RestoreBackupFileContent |
|
||||
restore_database=instruction_content["restore_database"],
|
||||
restore_homeassistant=instruction_content["restore_homeassistant"],
|
||||
)
|
||||
except (FileNotFoundError, KeyError, json.JSONDecodeError):
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (KeyError, json.JSONDecodeError) as err:
|
||||
_write_restore_result_file(config_dir, False, err)
|
||||
return None
|
||||
finally:
|
||||
# Always remove the backup instruction file to prevent a boot loop
|
||||
@ -159,6 +163,23 @@ def _extract_backup(
|
||||
)
|
||||
|
||||
|
||||
def _write_restore_result_file(
|
||||
config_dir: Path, success: bool, error: Exception | None
|
||||
) -> None:
|
||||
"""Write the restore result file."""
|
||||
result_path = config_dir.joinpath(RESTORE_BACKUP_RESULT_FILE)
|
||||
result_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"success": success,
|
||||
"error": str(error) if error else None,
|
||||
"error_type": str(type(error).__name__) if error else None,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def restore_backup(config_dir_path: str) -> bool:
|
||||
"""Restore the backup file if any.
|
||||
|
||||
@ -177,7 +198,14 @@ def restore_backup(config_dir_path: str) -> bool:
|
||||
restore_content=restore_content,
|
||||
)
|
||||
except FileNotFoundError as err:
|
||||
raise ValueError(f"Backup file {backup_file_path} does not exist") from err
|
||||
file_not_found = ValueError(f"Backup file {backup_file_path} does not exist")
|
||||
_write_restore_result_file(config_dir, False, file_not_found)
|
||||
raise file_not_found from err
|
||||
except Exception as err:
|
||||
_write_restore_result_file(config_dir, False, err)
|
||||
raise
|
||||
else:
|
||||
_write_restore_result_file(config_dir, True, None)
|
||||
if restore_content.remove_after_restore:
|
||||
backup_file_path.unlink(missing_ok=True)
|
||||
_LOGGER.info("Restore complete, restarting")
|
||||
|
@ -26,6 +26,7 @@ from .manager import (
|
||||
BackupReaderWriterError,
|
||||
CoreBackupReaderWriter,
|
||||
CreateBackupEvent,
|
||||
IdleEvent,
|
||||
IncorrectPasswordError,
|
||||
ManagerBackup,
|
||||
NewBackup,
|
||||
@ -47,6 +48,7 @@ __all__ = [
|
||||
"BackupReaderWriterError",
|
||||
"CreateBackupEvent",
|
||||
"Folder",
|
||||
"IdleEvent",
|
||||
"IncorrectPasswordError",
|
||||
"LocalBackupAgent",
|
||||
"ManagerBackup",
|
||||
|
@ -19,7 +19,11 @@ from typing import IO, TYPE_CHECKING, Any, Protocol, TypedDict, cast
|
||||
import aiohttp
|
||||
from securetar import SecureTarFile, atomic_contents_add
|
||||
|
||||
from homeassistant.backup_restore import RESTORE_BACKUP_FILE, password_to_key
|
||||
from homeassistant.backup_restore import (
|
||||
RESTORE_BACKUP_FILE,
|
||||
RESTORE_BACKUP_RESULT_FILE,
|
||||
password_to_key,
|
||||
)
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
@ -28,7 +32,7 @@ from homeassistant.helpers import (
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import dt as dt_util, json as json_util
|
||||
|
||||
from . import util as backup_util
|
||||
from .agent import (
|
||||
@ -261,6 +265,14 @@ class BackupReaderWriter(abc.ABC):
|
||||
) -> None:
|
||||
"""Restore a backup."""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def async_resume_restore_progress_after_restart(
|
||||
self,
|
||||
*,
|
||||
on_progress: Callable[[RestoreBackupEvent | IdleEvent], None],
|
||||
) -> None:
|
||||
"""Get restore events after core restart."""
|
||||
|
||||
|
||||
class BackupReaderWriterError(BackupError):
|
||||
"""Backup reader/writer error."""
|
||||
@ -318,6 +330,10 @@ class BackupManager:
|
||||
self.config.load(stored["config"])
|
||||
self.known_backups.load(stored["backups"])
|
||||
|
||||
await self._reader_writer.async_resume_restore_progress_after_restart(
|
||||
on_progress=self.async_on_backup_event
|
||||
)
|
||||
|
||||
await self.load_platforms()
|
||||
|
||||
@property
|
||||
@ -1605,6 +1621,54 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
)
|
||||
await self._hass.services.async_call("homeassistant", "restart", blocking=True)
|
||||
|
||||
async def async_resume_restore_progress_after_restart(
|
||||
self,
|
||||
*,
|
||||
on_progress: Callable[[RestoreBackupEvent | IdleEvent], None],
|
||||
) -> None:
|
||||
"""Check restore status after core restart."""
|
||||
|
||||
def _read_restore_file() -> json_util.JsonObjectType | None:
|
||||
"""Read the restore file."""
|
||||
result_path = Path(self._hass.config.path(RESTORE_BACKUP_RESULT_FILE))
|
||||
|
||||
try:
|
||||
restore_result = json_util.json_loads_object(result_path.read_bytes())
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
finally:
|
||||
try:
|
||||
result_path.unlink(missing_ok=True)
|
||||
except OSError as err:
|
||||
LOGGER.warning(
|
||||
"Unexpected error deleting backup restore result file: %s %s",
|
||||
type(err),
|
||||
err,
|
||||
)
|
||||
|
||||
return restore_result
|
||||
|
||||
restore_result = await self._hass.async_add_executor_job(_read_restore_file)
|
||||
if not restore_result:
|
||||
return
|
||||
|
||||
success = restore_result["success"]
|
||||
if not success:
|
||||
LOGGER.warning(
|
||||
"Backup restore failed with %s: %s",
|
||||
restore_result["error_type"],
|
||||
restore_result["error"],
|
||||
)
|
||||
state = RestoreBackupState.COMPLETED if success else RestoreBackupState.FAILED
|
||||
on_progress(
|
||||
RestoreBackupEvent(
|
||||
reason=cast(str, restore_result["error"]),
|
||||
stage=None,
|
||||
state=state,
|
||||
)
|
||||
)
|
||||
on_progress(IdleEvent())
|
||||
|
||||
|
||||
def _generate_backup_id(date: str, name: str) -> str:
|
||||
"""Generate a backup ID."""
|
||||
|
@ -60,8 +60,10 @@ async def handle_info(
|
||||
"backups": [backup.as_frontend_json() for backup in backups.values()],
|
||||
"last_attempted_automatic_backup": manager.config.data.last_attempted_automatic_backup,
|
||||
"last_completed_automatic_backup": manager.config.data.last_completed_automatic_backup,
|
||||
"last_non_idle_event": manager.last_non_idle_event,
|
||||
"next_automatic_backup": manager.config.data.schedule.next_automatic_backup,
|
||||
"next_automatic_backup_additional": manager.config.data.schedule.next_automatic_backup_additional,
|
||||
"state": manager.state,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -29,6 +29,7 @@ from homeassistant.components.backup import (
|
||||
BackupReaderWriterError,
|
||||
CreateBackupEvent,
|
||||
Folder,
|
||||
IdleEvent,
|
||||
IncorrectPasswordError,
|
||||
NewBackup,
|
||||
RestoreBackupEvent,
|
||||
@ -456,6 +457,13 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
|
||||
finally:
|
||||
unsub()
|
||||
|
||||
async def async_resume_restore_progress_after_restart(
|
||||
self,
|
||||
*,
|
||||
on_progress: Callable[[RestoreBackupEvent | IdleEvent], None],
|
||||
) -> None:
|
||||
"""Check restore status after core restart."""
|
||||
|
||||
@callback
|
||||
def _async_listen_job_events(
|
||||
self, job_id: str, on_event: Callable[[Mapping[str, Any]], None]
|
||||
|
@ -85,8 +85,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -117,8 +119,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -149,8 +153,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -181,8 +187,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -213,8 +221,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
|
@ -2977,8 +2977,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3005,8 +3007,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3050,8 +3054,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3078,8 +3084,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3123,8 +3131,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3179,8 +3189,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3219,8 +3231,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3270,8 +3284,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3319,8 +3335,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3375,8 +3393,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3432,8 +3452,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3490,8 +3512,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3546,8 +3570,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3602,8 +3628,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3658,8 +3686,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -3715,8 +3745,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -4181,8 +4213,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -4226,8 +4260,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -4275,8 +4311,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -4343,8 +4381,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
@ -4389,8 +4429,10 @@
|
||||
]),
|
||||
'last_attempted_automatic_backup': None,
|
||||
'last_completed_automatic_backup': None,
|
||||
'last_non_idle_event': None,
|
||||
'next_automatic_backup': None,
|
||||
'next_automatic_backup_additional': False,
|
||||
'state': 'idle',
|
||||
}),
|
||||
'success': True,
|
||||
'type': 'result',
|
||||
|
@ -397,8 +397,10 @@ async def test_initiate_backup(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -626,8 +628,10 @@ async def test_initiate_backup_with_agent_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
@ -724,8 +728,15 @@ async def test_initiate_backup_with_agent_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": {
|
||||
"manager_state": "create_backup",
|
||||
"reason": "upload_failed",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
},
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@ -993,8 +1004,10 @@ async def test_initiate_backup_non_agent_upload_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -1109,8 +1122,10 @@ async def test_initiate_backup_with_task_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -1217,8 +1232,10 @@ async def test_initiate_backup_file_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -1703,8 +1720,10 @@ async def test_receive_backup_agent_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id(
|
||||
@ -1786,8 +1805,15 @@ async def test_receive_backup_agent_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": {
|
||||
"manager_state": "receive_backup",
|
||||
"reason": None,
|
||||
"stage": None,
|
||||
"state": "completed",
|
||||
},
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
@ -1848,8 +1874,10 @@ async def test_receive_backup_non_agent_upload_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -1973,8 +2001,10 @@ async def test_receive_backup_file_write_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -2086,8 +2116,10 @@ async def test_receive_backup_read_tar_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -2264,8 +2296,10 @@ async def test_receive_backup_file_read_error(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
await ws_client.send_json_auto_id({"type": "backup/subscribe_events"})
|
||||
@ -3034,8 +3068,10 @@ async def test_initiate_backup_per_agent_encryption(
|
||||
"agent_errors": {},
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
for command in commands:
|
||||
@ -3127,3 +3163,88 @@ async def test_initiate_backup_per_agent_encryption(
|
||||
"name": "test",
|
||||
"with_automatic_settings": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("restore_result", "last_non_idle_event"),
|
||||
[
|
||||
(
|
||||
{"error": None, "error_type": None, "success": True},
|
||||
{
|
||||
"manager_state": "restore_backup",
|
||||
"reason": None,
|
||||
"stage": None,
|
||||
"state": "completed",
|
||||
},
|
||||
),
|
||||
(
|
||||
{"error": "Boom!", "error_type": "ValueError", "success": False},
|
||||
{
|
||||
"manager_state": "restore_backup",
|
||||
"reason": "Boom!",
|
||||
"stage": None,
|
||||
"state": "failed",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_restore_progress_after_restart(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
restore_result: dict[str, Any],
|
||||
last_non_idle_event: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test restore backup progress after restart."""
|
||||
|
||||
with patch(
|
||||
"pathlib.Path.read_bytes", return_value=json.dumps(restore_result).encode()
|
||||
):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"agent_errors": {},
|
||||
"backups": [],
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": last_non_idle_event,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
|
||||
async def test_restore_progress_after_restart_fail_to_remove(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test restore backup progress after restart when failing to remove result file."""
|
||||
|
||||
with patch("pathlib.Path.unlink", side_effect=OSError("Boom!")):
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ws_client = await hass_ws_client(hass)
|
||||
await ws_client.send_json_auto_id({"type": "backup/info"})
|
||||
result = await ws_client.receive_json()
|
||||
assert result["success"] is True
|
||||
assert result["result"] == {
|
||||
"agent_errors": {},
|
||||
"backups": [],
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
assert (
|
||||
"Unexpected error deleting backup restore result file: <class 'OSError'> Boom!"
|
||||
in caplog.text
|
||||
)
|
||||
|
@ -205,8 +205,10 @@ async def test_agents_list_backups_fail_cloud(
|
||||
"backups": [],
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
|
||||
|
@ -10,9 +10,12 @@
|
||||
'version': '1.0.0',
|
||||
}),
|
||||
]),
|
||||
'agent_ids': list([
|
||||
'backup.local',
|
||||
]),
|
||||
'agents': dict({
|
||||
'backup.local': dict({
|
||||
'protected': True,
|
||||
'size': 0,
|
||||
}),
|
||||
}),
|
||||
'backup_id': 'abc123',
|
||||
'database_included': True,
|
||||
'date': '1970-01-01T00:00:00.000Z',
|
||||
@ -25,16 +28,17 @@
|
||||
'homeassistant_included': True,
|
||||
'homeassistant_version': '2024.12.0',
|
||||
'name': 'Test',
|
||||
'protected': False,
|
||||
'size': 0,
|
||||
'with_automatic_settings': True,
|
||||
}),
|
||||
dict({
|
||||
'addons': list([
|
||||
]),
|
||||
'agent_ids': list([
|
||||
'test.remote',
|
||||
]),
|
||||
'agents': dict({
|
||||
'test.remote': dict({
|
||||
'protected': True,
|
||||
'size': 0,
|
||||
}),
|
||||
}),
|
||||
'backup_id': 'def456',
|
||||
'database_included': False,
|
||||
'date': '1980-01-01T00:00:00.000Z',
|
||||
@ -47,8 +51,6 @@
|
||||
'homeassistant_included': True,
|
||||
'homeassistant_version': '2024.12.0',
|
||||
'name': 'Test 2',
|
||||
'protected': False,
|
||||
'size': 1,
|
||||
'with_automatic_settings': None,
|
||||
}),
|
||||
]),
|
||||
|
@ -798,6 +798,9 @@ async def test_onboarding_backup_info(
|
||||
backups = {
|
||||
"abc123": backup.ManagerBackup(
|
||||
addons=[backup.AddonInfo(name="Test", slug="test", version="1.0.0")],
|
||||
agents={
|
||||
"backup.local": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="abc123",
|
||||
date="1970-01-01T00:00:00.000Z",
|
||||
database_included=True,
|
||||
@ -806,14 +809,14 @@ async def test_onboarding_backup_info(
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test",
|
||||
protected=False,
|
||||
size=0,
|
||||
agent_ids=["backup.local"],
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=True,
|
||||
),
|
||||
"def456": backup.ManagerBackup(
|
||||
addons=[],
|
||||
agents={
|
||||
"test.remote": backup.manager.AgentBackupStatus(protected=True, size=0)
|
||||
},
|
||||
backup_id="def456",
|
||||
date="1980-01-01T00:00:00.000Z",
|
||||
database_included=False,
|
||||
@ -825,9 +828,6 @@ async def test_onboarding_backup_info(
|
||||
homeassistant_included=True,
|
||||
homeassistant_version="2024.12.0",
|
||||
name="Test 2",
|
||||
protected=False,
|
||||
size=1,
|
||||
agent_ids=["test.remote"],
|
||||
failed_agent_ids=[],
|
||||
with_automatic_settings=None,
|
||||
),
|
||||
|
@ -332,8 +332,10 @@ async def test_agents_list_backups_error(
|
||||
"backups": [],
|
||||
"last_attempted_automatic_backup": None,
|
||||
"last_completed_automatic_backup": None,
|
||||
"last_non_idle_event": None,
|
||||
"next_automatic_backup": None,
|
||||
"next_automatic_backup_additional": False,
|
||||
"state": "idle",
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""Test methods in backup_restore."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@ -11,6 +14,23 @@ from homeassistant import backup_restore
|
||||
from .common import get_test_config_dir
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def remove_restore_result_file() -> Generator[None, Any, Any]:
|
||||
"""Remove the restore result file."""
|
||||
yield
|
||||
Path(get_test_config_dir(".HA_RESTORE_RESULT")).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def restore_result_file_content() -> dict[str, Any] | None:
|
||||
"""Return the content of the restore result file."""
|
||||
try:
|
||||
return json.loads(
|
||||
Path(get_test_config_dir(".HA_RESTORE_RESULT")).read_text("utf-8")
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "content", "expected"),
|
||||
[
|
||||
@ -87,6 +107,11 @@ def test_restoring_backup_that_does_not_exist() -> None:
|
||||
),
|
||||
):
|
||||
assert backup_restore.restore_backup(Path(get_test_config_dir())) is False
|
||||
assert restore_result_file_content() == {
|
||||
"error": f"Backup file {backup_file_path} does not exist",
|
||||
"error_type": "ValueError",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
def test_restoring_backup_when_instructions_can_not_be_read() -> None:
|
||||
@ -98,6 +123,7 @@ def test_restoring_backup_when_instructions_can_not_be_read() -> None:
|
||||
),
|
||||
):
|
||||
assert backup_restore.restore_backup(Path(get_test_config_dir())) is False
|
||||
assert restore_result_file_content() is None
|
||||
|
||||
|
||||
def test_restoring_backup_that_is_not_a_file() -> None:
|
||||
@ -121,6 +147,11 @@ def test_restoring_backup_that_is_not_a_file() -> None:
|
||||
),
|
||||
):
|
||||
assert backup_restore.restore_backup(Path(get_test_config_dir())) is False
|
||||
assert restore_result_file_content() == {
|
||||
"error": f"Backup file {backup_file_path} does not exist",
|
||||
"error_type": "ValueError",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
def test_aborting_for_older_versions() -> None:
|
||||
@ -152,6 +183,13 @@ def test_aborting_for_older_versions() -> None:
|
||||
),
|
||||
):
|
||||
assert backup_restore.restore_backup(config_dir) is True
|
||||
assert restore_result_file_content() == {
|
||||
"error": (
|
||||
"You need at least Home Assistant version 9999.99.99 to restore this backup"
|
||||
),
|
||||
"error_type": "ValueError",
|
||||
"success": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -280,6 +318,11 @@ def test_removal_of_current_configuration_when_restoring(
|
||||
assert removed_directories == {
|
||||
Path(config_dir, d) for d in expected_removed_directories
|
||||
}
|
||||
assert restore_result_file_content() == {
|
||||
"error": None,
|
||||
"error_type": None,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
def test_extracting_the_contents_of_a_backup_file() -> None:
|
||||
@ -332,6 +375,11 @@ def test_extracting_the_contents_of_a_backup_file() -> None:
|
||||
assert {
|
||||
member.name for member in extractall_mock.mock_calls[-1].kwargs["members"]
|
||||
} == {"data", "data/.HA_VERSION", "data/.storage", "data/www"}
|
||||
assert restore_result_file_content() == {
|
||||
"error": None,
|
||||
"error_type": None,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -362,6 +410,11 @@ def test_remove_backup_file_after_restore(
|
||||
assert mock_unlink.call_count == unlink_calls
|
||||
for call in mock_unlink.mock_calls:
|
||||
assert call.args[0] == backup_file_path
|
||||
assert restore_result_file_content() == {
|
||||
"error": None,
|
||||
"error_type": None,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
Loading…
x
Reference in New Issue
Block a user