Allow restarting core in safe mode (#5017)

This commit is contained in:
Mike Degatano 2024-04-17 02:54:56 -04:00 committed by GitHub
parent b4a79bd068
commit 06513e88c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 47 additions and 3 deletions

View File

@ -53,6 +53,7 @@ ATTR_PANEL_PATH = "panel_path"
ATTR_REMOVABLE = "removable" ATTR_REMOVABLE = "removable"
ATTR_REMOVE_CONFIG = "remove_config" ATTR_REMOVE_CONFIG = "remove_config"
ATTR_REVISION = "revision" ATTR_REVISION = "revision"
ATTR_SAFE_MODE = "safe_mode"
ATTR_SEAT = "seat" ATTR_SEAT = "seat"
ATTR_SIGNED = "signed" ATTR_SIGNED = "signed"
ATTR_STARTUP_TIME = "startup_time" ATTR_STARTUP_TIME = "startup_time"

View File

@ -36,6 +36,7 @@ from ..const import (
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..validate import docker_image, network_port, version_tag from ..validate import docker_image, network_port, version_tag
from .const import ATTR_SAFE_MODE
from .utils import api_process, api_validate from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -62,6 +63,12 @@ SCHEMA_UPDATE = vol.Schema(
} }
) )
SCHEMA_RESTART = vol.Schema(
{
vol.Optional(ATTR_SAFE_MODE, default=False): vol.Boolean(),
}
)
class APIHomeAssistant(CoreSysAttributes): class APIHomeAssistant(CoreSysAttributes):
"""Handle RESTful API for Home Assistant functions.""" """Handle RESTful API for Home Assistant functions."""
@ -166,9 +173,13 @@ class APIHomeAssistant(CoreSysAttributes):
return asyncio.shield(self.sys_homeassistant.core.start()) return asyncio.shield(self.sys_homeassistant.core.start())
@api_process @api_process
def restart(self, request: web.Request) -> Awaitable[None]: async def restart(self, request: web.Request) -> None:
"""Restart Home Assistant.""" """Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.core.restart()) body = await api_validate(SCHEMA_RESTART, request)
await asyncio.shield(
self.sys_homeassistant.core.restart(safe_mode=body[ATTR_SAFE_MODE])
)
@api_process @api_process
def rebuild(self, request: web.Request) -> Awaitable[None]: def rebuild(self, request: web.Request) -> Awaitable[None]:

View File

@ -1,6 +1,7 @@
"""Constants for homeassistant.""" """Constants for homeassistant."""
from datetime import timedelta from datetime import timedelta
from enum import StrEnum from enum import StrEnum
from pathlib import PurePath
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -12,6 +13,7 @@ WATCHDOG_RETRY_SECONDS = 10
WATCHDOG_MAX_ATTEMPTS = 5 WATCHDOG_MAX_ATTEMPTS = 5
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30) WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
WATCHDOG_THROTTLE_MAX_CALLS = 10 WATCHDOG_THROTTLE_MAX_CALLS = 10
SAFE_MODE_FILENAME = PurePath("safe-mode")
CLOSING_STATES = [ CLOSING_STATES = [
CoreState.SHUTDOWN, CoreState.SHUTDOWN,

View File

@ -35,6 +35,7 @@ from ..utils import convert_to_ascii
from ..utils.sentry import capture_exception from ..utils.sentry import capture_exception
from .const import ( from .const import (
LANDINGPAGE, LANDINGPAGE,
SAFE_MODE_FILENAME,
WATCHDOG_MAX_ATTEMPTS, WATCHDOG_MAX_ATTEMPTS,
WATCHDOG_RETRY_SECONDS, WATCHDOG_RETRY_SECONDS,
WATCHDOG_THROTTLE_MAX_CALLS, WATCHDOG_THROTTLE_MAX_CALLS,
@ -362,8 +363,14 @@ class HomeAssistantCore(JobGroup):
limit=JobExecutionLimit.GROUP_ONCE, limit=JobExecutionLimit.GROUP_ONCE,
on_condition=HomeAssistantJobError, on_condition=HomeAssistantJobError,
) )
async def restart(self) -> None: async def restart(self, *, safe_mode: bool = False) -> None:
"""Restart Home Assistant Docker.""" """Restart Home Assistant Docker."""
# Create safe mode marker file if necessary
if safe_mode:
await self.sys_run_in_executor(
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
)
try: try:
await self.instance.restart() await self.instance.restart()
except DockerError as err: except DockerError as err:

View File

@ -1,11 +1,13 @@
"""Test homeassistant api.""" """Test homeassistant api."""
from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant from supervisor.homeassistant.module import HomeAssistant
from tests.api import common_test_api_advanced_logs from tests.api import common_test_api_advanced_logs
@ -92,3 +94,24 @@ async def test_api_set_image(api_client: TestClient, coresys: CoreSys):
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant" coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
) )
assert coresys.homeassistant.override_image is False assert coresys.homeassistant.override_image is False
async def test_api_restart(
api_client: TestClient,
container: MagicMock,
tmp_supervisor_data: Path,
):
"""Test restarting homeassistant."""
safe_mode_marker = tmp_supervisor_data / "homeassistant" / "safe-mode"
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/restart")
container.restart.assert_called_once()
assert not safe_mode_marker.exists()
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/restart", json={"safe_mode": True})
assert container.restart.call_count == 2
assert safe_mode_marker.exists()