diff --git a/supervisor/docker/interface.py b/supervisor/docker/interface.py index 680349156..c84ecbdfa 100644 --- a/supervisor/docker/interface.py +++ b/supervisor/docker/interface.py @@ -99,6 +99,13 @@ class DockerInterface(CoreSysAttributes): docker_image.tag(image, tag="latest") except docker.errors.APIError as err: _LOGGER.error("Can't install %s:%s -> %s.", image, tag, err) + if err.status_code == 404: + free_space = self.sys_host.info.free_space + _LOGGER.info( + "This error is often caused by not having enough disk space available. " + "Available space in /data is: %s GiB", + free_space, + ) raise DockerAPIError() from None else: self._meta = docker_image.attrs diff --git a/supervisor/host/info.py b/supervisor/host/info.py index 5d02b1a5b..66bcb05f3 100644 --- a/supervisor/host/info.py +++ b/supervisor/host/info.py @@ -51,6 +51,13 @@ class InfoCenter(CoreSysAttributes): """Return local CPE.""" return self.sys_dbus.hostname.cpe + @property + def free_space(self) -> float: + """Return available space (GiB) on disk for supervisor data directory.""" + return self.coresys.hardware.get_disk_free_space( + self.coresys.config.path_supervisor + ) + async def get_dmesg(self) -> bytes: """Return host dmesg output.""" proc = await asyncio.create_subprocess_shell( diff --git a/supervisor/misc/hardware.py b/supervisor/misc/hardware.py index 89f4841cc..addd90065 100644 --- a/supervisor/misc/hardware.py +++ b/supervisor/misc/hardware.py @@ -4,7 +4,8 @@ from datetime import datetime import logging from pathlib import Path import re -from typing import Any, Dict, List, Optional, Set +import shutil +from typing import Any, Dict, List, Optional, Set, Union import attr import pyudev @@ -195,6 +196,11 @@ class Hardware: return datetime.utcfromtimestamp(int(found.group(1))) + def get_disk_free_space(self, path: Union[str, Path]) -> float: + """Return free space (GiB) on disk for path.""" + _, _, free = shutil.disk_usage(path) + return round(free / (1024.0 ** 3), 1) + async def udev_trigger(self) -> None: """Trigger a udev reload.""" proc = await asyncio.create_subprocess_shell( diff --git a/tests/host/test_info.py b/tests/host/test_info.py new file mode 100644 index 000000000..664f65056 --- /dev/null +++ b/tests/host/test_info.py @@ -0,0 +1,13 @@ +"""Test host info.""" +from unittest.mock import patch + +from supervisor.host.info import InfoCenter + + +def test_host_free_space(coresys): + """Test host free space.""" + info = InfoCenter(coresys) + with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): + free = info.free_space + + assert free == 2.0 diff --git a/tests/misc/test_hardware.py b/tests/misc/test_hardware.py index 5ac03fc5f..cbe2eda30 100644 --- a/tests/misc/test_hardware.py +++ b/tests/misc/test_hardware.py @@ -32,3 +32,12 @@ def test_video_devices(): Device("cec0", Path("/dev/cec0"), []), Device("video1", Path("/dev/video1"), []), ] + + +def test_free_space(): + """Test free space helper.""" + system = Hardware() + with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))): + free = system.get_disk_free_space("/data") + + assert free == 2.0