Skip to content

Commit

Permalink
Add env on core restart due to restore
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 committed Jan 14, 2025
1 parent b07236b commit cad497e
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 33 deletions.
2 changes: 1 addition & 1 deletion supervisor/backups/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ async def store_homeassistant(self, exclude_database: bool = False):
@Job(name="backup_restore_homeassistant", cleanup=False)
async def restore_homeassistant(self) -> Awaitable[None]:
"""Restore Home Assistant Core configuration folder."""
await self.sys_homeassistant.core.stop()
await self.sys_homeassistant.core.stop(remove_container=True)

# Restore Home Assistant Core config directory
tar_name = Path(
Expand Down
1 change: 0 additions & 1 deletion supervisor/backups/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class RestoreJobStage(StrEnum):
ADDONS = "addons"
AWAIT_ADDON_RESTARTS = "await_addon_restarts"
AWAIT_HOME_ASSISTANT_RESTART = "await_home_assistant_restart"
CHECK_HOME_ASSISTANT = "check_home_assistant"
DOCKER_CONFIG = "docker_config"
FOLDERS = "folders"
HOME_ASSISTANT = "home_assistant"
Expand Down
7 changes: 2 additions & 5 deletions supervisor/backups/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,9 +619,6 @@ async def _do_restore(

# Wait for Home Assistant Core update/downgrade
if task_hass:
self._change_stage(
RestoreJobStage.AWAIT_HOME_ASSISTANT_RESTART, backup
)
await task_hass
except BackupError:
raise
Expand All @@ -644,7 +641,7 @@ async def _do_restore(
finally:
# Leave Home Assistant alone if it wasn't part of the restore
if homeassistant:
self._change_stage(RestoreJobStage.CHECK_HOME_ASSISTANT, backup)
self._change_stage(RestoreJobStage.AWAIT_HOME_ASSISTANT_RESTART, backup)

# Do we need start Home Assistant Core?
if not await self.sys_homeassistant.core.is_running():
Expand Down Expand Up @@ -706,7 +703,7 @@ async def do_restore_full(

try:
# Stop Home-Assistant / Add-ons
await self.sys_core.shutdown()
await self.sys_core.shutdown(remove_homeassistant_container=True)

success = await self._do_restore(
backup,
Expand Down
6 changes: 4 additions & 2 deletions supervisor/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ async def stop(self):
_LOGGER.info("Supervisor is down - %d", self.exit_code)
self.sys_loop.stop()

async def shutdown(self):
async def shutdown(self, *, remove_homeassistant_container: bool = False):
"""Shutdown all running containers in correct order."""
# don't process scheduler anymore
if self.state == CoreState.RUNNING:
Expand All @@ -344,7 +344,9 @@ async def shutdown(self):

# Close Home Assistant
with suppress(HassioError):
await self.sys_homeassistant.core.stop()
await self.sys_homeassistant.core.stop(
remove_container=remove_homeassistant_container
)

# Shutdown System Add-ons
await self.sys_addons.shutdown(AddonStartup.SERVICES)
Expand Down
20 changes: 12 additions & 8 deletions supervisor/docker/homeassistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
_VERIFY_TRUST: AwesomeVersion = AwesomeVersion("2021.5.0")
_HASS_DOCKER_NAME: str = "homeassistant"
ENV_S6_GRACETIME = re.compile(r"^S6_SERVICES_GRACETIME=([0-9]+)$")
ENV_RESTORE_JOB_ID = "SUPERVISOR_RESTORE_JOB_ID"


class DockerHomeAssistant(DockerInterface):
Expand Down Expand Up @@ -163,8 +164,17 @@ def mounts(self) -> list[Mount]:
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=DockerJobError,
)
async def run(self) -> None:
async def run(self, *, restore_job_id: str | None = None) -> None:
"""Run Docker image."""
environment = {
"SUPERVISOR": self.sys_docker.network.supervisor,
"HASSIO": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.supervisor_token,
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
}
if restore_job_id:
environment[ENV_RESTORE_JOB_ID] = restore_job_id
await self._run(
tag=(self.sys_homeassistant.version),
name=self.name,
Expand All @@ -180,13 +190,7 @@ async def run(self) -> None:
"supervisor": self.sys_docker.network.supervisor,
"observer": self.sys_docker.network.observer,
},
environment={
"SUPERVISOR": self.sys_docker.network.supervisor,
"HASSIO": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.supervisor_token,
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
},
environment=environment,
tmpfs={"/tmp": ""}, # noqa: S108
oom_score_adj=-300,
)
Expand Down
18 changes: 15 additions & 3 deletions supervisor/homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,18 @@ async def _update(to_version: AwesomeVersion) -> None:
self.sys_resolution.create_issue(IssueType.UPDATE_FAILED, ContextType.CORE)
raise HomeAssistantUpdateError()

def _is_restore(self) -> str | None:
"""Return id of current restore job if a restore job is in progress."""
job = self.sys_jobs.current
while job.parent_id:
job = self.sys_jobs.get_job(job.parent_id)
if job.name in {
"backup_manager_full_restore",
"backup_manager_partial_restore",
}:
return job.uuid
return None

@Job(
name="home_assistant_core_start",
limit=JobExecutionLimit.GROUP_ONCE,
Expand Down Expand Up @@ -345,7 +357,7 @@ async def start(self) -> None:
self.sys_homeassistant.write_pulse()

try:
await self.instance.run()
await self.instance.run(restore_job_id=self._is_restore())
except DockerError as err:
raise HomeAssistantError() from err

Expand All @@ -356,10 +368,10 @@ async def start(self) -> None:
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=HomeAssistantJobError,
)
async def stop(self) -> None:
async def stop(self, *, remove_container: bool = False) -> None:
"""Stop Home Assistant Docker."""
try:
return await self.instance.stop(remove_container=False)
return await self.instance.stop(remove_container=remove_container)
except DockerError as err:
raise HomeAssistantError() from err

Expand Down
12 changes: 6 additions & 6 deletions supervisor/jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from datetime import datetime
import logging
from typing import Any
from uuid import UUID, uuid4
from uuid import uuid4

from attrs import Attribute, define, field
from attrs.setters import convert as attr_convert, frozen, validate as attr_validate
Expand All @@ -27,7 +27,7 @@
# When a new asyncio task is started the current context is copied over.
# Modifications to it in one task are not visible to others though.
# This allows us to track what job is currently in progress in each task.
_CURRENT_JOB: ContextVar[UUID] = ContextVar("current_job")
_CURRENT_JOB: ContextVar[str] = ContextVar("current_job")

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -94,8 +94,8 @@ class SupervisorJob:
stage: str | None = field(
default=None, validator=[_invalid_if_done], on_setattr=_on_change
)
uuid: UUID = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
parent_id: UUID | None = field(
uuid: str = field(init=False, factory=lambda: uuid4().hex, on_setattr=frozen)
parent_id: str | None = field(
factory=lambda: _CURRENT_JOB.get(None), on_setattr=frozen
)
done: bool | None = field(init=False, default=None, on_setattr=_on_change)
Expand Down Expand Up @@ -143,7 +143,7 @@ def start(self):
raise JobStartException("Job has a different parent from current job")

self.done = False
token: Token[UUID] | None = None
token: Token[str] | None = None
try:
token = _CURRENT_JOB.set(self.uuid)
yield self
Expand Down Expand Up @@ -234,7 +234,7 @@ def new_job(
self._jobs[job.uuid] = job
return job

def get_job(self, uuid: UUID) -> SupervisorJob:
def get_job(self, uuid: str) -> SupervisorJob:
"""Return a job by uuid. Raises if it does not exist."""
if uuid not in self._jobs:
raise JobNotFound(f"No job found with id {uuid}")
Expand Down
57 changes: 57 additions & 0 deletions tests/api/test_backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
from supervisor.backups.backup import Backup
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.docker.manager import DockerAPI
from supervisor.exceptions import AddonsError, HomeAssistantBackupError
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
from supervisor.homeassistant.websocket import HomeAssistantWebSocket
from supervisor.mounts.mount import Mount
from supervisor.supervisor import Supervisor

Expand Down Expand Up @@ -857,3 +859,58 @@ async def test_restore_backup_from_location(
)
assert resp.status == 200
assert test_file.is_file()


@pytest.mark.parametrize(
("backup_type", "postbody"), [("partial", {"homeassistant": True}), ("full", {})]
)
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
async def test_restore_homeassistant_adds_env(
api_client: TestClient,
coresys: CoreSys,
docker: DockerAPI,
backup_type: str,
postbody: dict[str, Any],
):
"""Test restoring home assistant from backup adds env to container."""
event = asyncio.Event()
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
coresys.homeassistant.version = AwesomeVersion("2025.1.0")
backup = await coresys.backups.do_backup_full()

async def mock_async_send_message(_, message: dict[str, Any]):
"""Mock of async send message in ws client."""
if (
message["data"]["event"] == "job"
and message["data"]["data"]["name"]
== f"backup_manager_{backup_type}_restore"
and message["data"]["data"]["reference"] == backup.slug
and message["data"]["data"]["done"]
):
event.set()

with (
patch.object(HomeAssistantCore, "_block_till_run"),
patch.object(
HomeAssistantWebSocket, "async_send_message", new=mock_async_send_message
),
):
resp = await api_client.post(
f"/backups/{backup.slug}/restore/{backup_type}",
json={"background": True} | postbody,
)
assert resp.status == 200
body = await resp.json()
job = coresys.jobs.get_job(body["data"]["job_id"])

if not job.done:
await asyncio.wait_for(event.wait(), 5)

assert docker.containers.create.call_args.kwargs["name"] == "homeassistant"
assert (
docker.containers.create.call_args.kwargs["environment"][
"SUPERVISOR_RESTORE_JOB_ID"
]
== job.uuid
)
9 changes: 2 additions & 7 deletions tests/backups/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,11 +1248,6 @@ async def test_restore_progress(
_make_backup_message_for_assert(
action="full_restore", reference=full_backup.slug, stage="addons"
),
_make_backup_message_for_assert(
action="full_restore",
reference=full_backup.slug,
stage="await_home_assistant_restart",
),
_make_backup_message_for_assert(
action="full_restore",
reference=full_backup.slug,
Expand All @@ -1261,12 +1256,12 @@ async def test_restore_progress(
_make_backup_message_for_assert(
action="full_restore",
reference=full_backup.slug,
stage="check_home_assistant",
stage="await_home_assistant_restart",
),
_make_backup_message_for_assert(
action="full_restore",
reference=full_backup.slug,
stage="check_home_assistant",
stage="await_home_assistant_restart",
done=True,
),
]
Expand Down

0 comments on commit cad497e

Please sign in to comment.