Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions supervisor/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,7 @@ def _register_docker(self, app: web.Application) -> None:
web.get("/docker/registries", api_docker.registries),
web.post("/docker/registries", api_docker.create_registry),
web.delete("/docker/registries/{hostname}", api_docker.remove_registry),
web.post("/docker/reset-storage", api_docker.reset_storage),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/docker is allowed by Supervisor API manager role. Is that ok for this API as well? 🤔 I guess /docker/migrate-storage-driver is very similar and already enabled for the manager role 🤔 . For me manager is fine, just wanted to bring it up so this is a conscious decision.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. But if we want to restrict that, let's also restrict other "dangerous" endpoints in a follow-up.

]
)

Expand Down
33 changes: 32 additions & 1 deletion supervisor/api/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
ATTR_VERSION,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APINotFound
from ..exceptions import APIError, APINotFound, DBusError
from ..resolution.const import ContextType, IssueType, SuggestionType
from .utils import api_process, api_validate

Expand Down Expand Up @@ -154,3 +154,34 @@ async def migrate_docker_storage_driver(self, request: web.Request) -> None:
ContextType.SYSTEM,
suggestions=[SuggestionType.EXECUTE_REBOOT],
)

@api_process
async def reset_storage(self, request: web.Request) -> None:
"""Schedule a Docker storage reset on next reboot."""
if (
not self.coresys.os.available
or not self.coresys.os.version
or self.coresys.os.version < AwesomeVersion("18.3.dev0")
Comment thread
agners marked this conversation as resolved.
Outdated
):
raise APINotFound(
"Home Assistant OS 18.3 or newer required for Docker storage reset"
)

_LOGGER.info("Scheduling reset of Docker storage on next reboot")
try:
if not await self.sys_dbus.agent.system.schedule_docker_storage_reset():
raise APIError(
"Can't schedule Docker storage reset, check host logs for details",
_LOGGER.error,
)
except DBusError as err:
raise APIError(
f"Can't schedule Docker storage reset: {err!s}", _LOGGER.error
) from err

_LOGGER.info("Host system reboot required to apply Docker storage reset")
self.sys_resolution.create_issue(
IssueType.REBOOT_REQUIRED,
ContextType.SYSTEM,
suggestions=[SuggestionType.EXECUTE_REBOOT],
)
5 changes: 5 additions & 0 deletions supervisor/dbus/agent/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ async def schedule_wipe_device(self) -> bool:
async def migrate_docker_storage_driver(self, backend: str) -> None:
"""Migrate Docker storage driver."""
await self.connected_dbus.System.call("migrate_docker_storage_driver", backend)

@dbus_connected
async def schedule_docker_storage_reset(self) -> bool:
"""Schedule a Docker storage reset on next system boot."""
return await self.connected_dbus.System.call("schedule_docker_storage_reset")
102 changes: 102 additions & 0 deletions tests/api/test_docker.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test Docker API."""

from aiohttp.test_utils import TestClient
from dbus_fast import DBusError, ErrorType
import pytest

from supervisor.coresys import CoreSys
Expand Down Expand Up @@ -170,3 +171,104 @@ async def test_api_migrate_docker_storage_driver_old_os(
json={"storage_driver": "overlayfs"},
)
assert resp.status == 404


@pytest.mark.parametrize("os_available", ["18.3.dev0"], indirect=True)
async def test_api_docker_reset_storage(
coresys: CoreSys,
api_client_with_prefix: tuple[TestClient, str],
os_agent_services: dict[str, DBusServiceMock],
os_available,
):
"""Test Docker storage reset."""
api_client, prefix = api_client_with_prefix
system_service: SystemService = os_agent_services["agent_system"]
system_service.ScheduleDockerStorageReset.calls.clear()

resp = await api_client.post(f"{prefix}/docker/reset-storage")
assert resp.status == 200

assert system_service.ScheduleDockerStorageReset.calls == [()]
assert (
Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
in coresys.resolution.issues
)
assert (
Suggestion(SuggestionType.EXECUTE_REBOOT, ContextType.SYSTEM)
in coresys.resolution.suggestions
)


async def test_api_docker_reset_storage_not_os(
api_client_with_prefix: tuple[TestClient, str],
):
"""Test 404 is returned if not running on HAOS."""
api_client, prefix = api_client_with_prefix
resp = await api_client.post(f"{prefix}/docker/reset-storage")
assert resp.status == 404


@pytest.mark.parametrize("os_available", ["18.2"], indirect=True)
async def test_api_docker_reset_storage_old_os(
api_client_with_prefix: tuple[TestClient, str],
os_available,
):
"""Test 404 is returned if OS is older than 18.3."""
api_client, prefix = api_client_with_prefix
resp = await api_client.post(f"{prefix}/docker/reset-storage")
assert resp.status == 404


@pytest.mark.parametrize("os_available", ["18.3.dev0"], indirect=True)
async def test_api_docker_reset_storage_schedule_failed(
coresys: CoreSys,
api_client_with_prefix: tuple[TestClient, str],
os_agent_services: dict[str, DBusServiceMock],
os_available,
):
"""Test error if OS Agent could not schedule the reset."""
api_client, prefix = api_client_with_prefix
system_service: SystemService = os_agent_services["agent_system"]
system_service.ScheduleDockerStorageReset.calls.clear()
system_service.response_schedule_docker_storage_reset = False

resp = await api_client.post(f"{prefix}/docker/reset-storage")
assert resp.status == 400
body = await resp.json()
assert (
body["message"]
== "Can't schedule Docker storage reset, check host logs for details"
)

assert system_service.ScheduleDockerStorageReset.calls == [()]
assert (
Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
not in coresys.resolution.issues
)


@pytest.mark.parametrize("os_available", ["18.3.dev0"], indirect=True)
async def test_api_docker_reset_storage_dbus_error(
coresys: CoreSys,
api_client_with_prefix: tuple[TestClient, str],
os_agent_services: dict[str, DBusServiceMock],
os_available,
):
"""Test error if the D-Bus call fails."""
api_client, prefix = api_client_with_prefix
system_service: SystemService = os_agent_services["agent_system"]
system_service.ScheduleDockerStorageReset.calls.clear()
system_service.response_schedule_docker_storage_reset = DBusError(
ErrorType.FAILED, "fail"
)

resp = await api_client.post(f"{prefix}/docker/reset-storage")
assert resp.status == 400
body = await resp.json()
assert body["message"] == "Can't schedule Docker storage reset: fail"

assert system_service.ScheduleDockerStorageReset.calls == [()]
assert (
Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
not in coresys.resolution.issues
)
16 changes: 16 additions & 0 deletions tests/dbus/agent/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@ async def test_dbus_osagent_system_wipe(

assert await os_agent.system.schedule_wipe_device() is True
assert system_service.ScheduleWipeDevice.calls == [()]


async def test_dbus_osagent_system_schedule_docker_storage_reset(
system_service: SystemService, dbus_session_bus: MessageBus
):
"""Test scheduling Docker storage reset on host."""
system_service.ScheduleDockerStorageReset.calls.clear()
os_agent = OSAgent()

with pytest.raises(DBusNotConnectedError):
await os_agent.system.schedule_docker_storage_reset()

await os_agent.connect(dbus_session_bus)

assert await os_agent.system.schedule_docker_storage_reset() is True
assert system_service.ScheduleDockerStorageReset.calls == [()]
8 changes: 8 additions & 0 deletions tests/dbus_service_mocks/agent_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class System(DBusServiceMock):
interface = "io.hass.os.System"
response_schedule_wipe_device: bool | DBusError = True
response_migrate_docker_storage_driver: None | DBusError = None
response_schedule_docker_storage_reset: bool | DBusError = True

@dbus_method()
def ScheduleWipeDevice(self) -> "b":
Expand All @@ -40,3 +41,10 @@ def MigrateDockerStorageDriver(self, backend: "s") -> None:
ErrorType.FAILED,
f"unsupported driver: {backend} (only 'overlayfs' is currently supported)",
)

@dbus_method()
def ScheduleDockerStorageReset(self) -> "b":
"""Schedule Docker storage reset."""
if isinstance(self.response_schedule_docker_storage_reset, DBusError):
raise self.response_schedule_docker_storage_reset # pylint: disable=raising-bad-type
return self.response_schedule_docker_storage_reset