Compare commits

...

6 Commits

Author SHA1 Message Date
Pascal Vizeli
49ca923e51 Apply old udev handling like core container (#2511) 2021-02-03 12:56:25 +01:00
Pascal Vizeli
7ad22e0399 Make add-on file schema more future approved (#2510) 2021-02-03 12:06:22 +01:00
Pascal Vizeli
bb8acc6065 Fix add-on state was not fast reflected (#2509) 2021-02-03 12:02:09 +01:00
Joakim Sørensen
c0fa4a19e9 Change verbosity for hardware.trigger API (#2508) 2021-02-02 20:56:37 +01:00
Franck Nijhof
3f1741dd18 Typo: depircated -> deprecated (#2507) 2021-02-02 20:41:56 +01:00
Stefan Agner
9ef02e4110 Add eMMC life-time estimate support (#2413)
* Add eMMC life-time estimate support

Expose life time estimate as Host Info property "ssd_life_time"

* Fix pytest

* Fix path to helper

* Allow protected access in tests

* Apply suggestions from code review

Rename SSD to disk.

Co-authored-by: Pascal Vizeli <pascal.vizeli@syshack.ch>

* Rename functions as well

* Update host.py

Co-authored-by: Pascal Vizeli <pascal.vizeli@syshack.ch>
2021-02-02 10:31:02 +01:00
10 changed files with 123 additions and 7 deletions

View File

@@ -3,15 +3,16 @@
# Start udev service
# ==============================================================================
if bashio::fs.directory_exists /run/udev; then
if bashio::fs.directory_exists /run/udev && ! bashio::fs.file_exists /run/.old_udev; then
bashio::log.info "Using udev information from host"
bashio::exit.ok
fi
bashio::log.info "Setup udev backend inside container"
udevd --daemon
bashio::log.info "Update udev information"
touch /run/.old_udev
if udevadm trigger; then
udevadm settle || true
else

View File

@@ -594,7 +594,7 @@ class Addon(AddonModel):
async def stop(self) -> None:
"""Stop add-on."""
try:
return await self.instance.stop()
await self.instance.stop()
except DockerRequestError as err:
raise AddonsError() from err
except DockerError as err:

View File

@@ -149,7 +149,7 @@ def _migrate_addon_config(protocol=False):
value = config[ATTR_STARTUP]
if protocol:
_LOGGER.warning(
"Add-on config 'startup' with '%s' is depircated. Please report this to the maintainer of %s",
"Add-on config 'startup' with '%s' is deprecated. Please report this to the maintainer of %s",
value,
name,
)
@@ -334,7 +334,8 @@ SCHEMA_ADDONS_FILE = vol.Schema(
{
vol.Optional(ATTR_USER, default=dict): {str: SCHEMA_ADDON_USER},
vol.Optional(ATTR_SYSTEM, default=dict): {str: SCHEMA_ADDON_SYSTEM},
}
},
extra=vol.REMOVE_EXTRA,
)

View File

@@ -62,4 +62,4 @@ class APIHardware(CoreSysAttributes):
@api_process
async def trigger(self, request: web.Request) -> Awaitable[None]:
"""Trigger a udev device reload."""
_LOGGER.warning("Ignoring DEPRECATED hardware trigger function call.")
_LOGGER.debug("Ignoring DEPRECATED hardware trigger function call.")

View File

@@ -11,6 +11,7 @@ from ..const import (
ATTR_DEPLOYMENT,
ATTR_DESCRIPTON,
ATTR_DISK_FREE,
ATTR_DISK_LIFE_TIME,
ATTR_DISK_TOTAL,
ATTR_DISK_USED,
ATTR_FEATURES,
@@ -43,6 +44,7 @@ class APIHost(CoreSysAttributes):
ATTR_DISK_FREE: self.sys_host.info.free_space,
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
ATTR_DISK_USED: self.sys_host.info.used_space,
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
ATTR_FEATURES: self.sys_host.features,
ATTR_HOSTNAME: self.sys_host.info.hostname,
ATTR_KERNEL: self.sys_host.info.kernel,

View File

@@ -236,6 +236,7 @@ ATTR_SNAPSHOTS = "snapshots"
ATTR_SOURCE = "source"
ATTR_SQUASH = "squash"
ATTR_SSID = "ssid"
ATTR_DISK_LIFE_TIME = "disk_life_time"
ATTR_SSL = "ssl"
ATTR_STAGE = "stage"
ATTR_STARTUP = "startup"

View File

@@ -19,6 +19,10 @@ _RE_BOOT_TIME: re.Pattern = re.compile(r"btime (\d+)")
_RE_HIDE_SYSFS: re.Pattern = re.compile(r"/sys/devices/virtual/(?:tty|block|vc)/.*")
_MOUNTINFO: Path = Path("/proc/self/mountinfo")
_BLOCK_DEVICE_CLASS = "/sys/class/block/{}"
_BLOCK_DEVICE_EMMC_LIFE_TIME = "/sys/block/{}/device/life_time"
class HwHelper(CoreSysAttributes):
"""Representation of an interface to procfs, sysfs and udev."""
@@ -78,3 +82,76 @@ class HwHelper(CoreSysAttributes):
"""Return free space (GiB) on disk for path."""
_, _, free = shutil.disk_usage(path)
return round(free / (1024.0 ** 3), 1)
def _get_mountinfo(self, path: str) -> str:
mountinfo = _MOUNTINFO.read_text()
for line in mountinfo.splitlines():
mountinfoarr = line.split()
if mountinfoarr[4] == path:
return mountinfoarr
return None
def _get_mount_source(self, path: str) -> str:
mountinfoarr = self._get_mountinfo(path)
if mountinfoarr is None:
return None
# Find optional field separator
optionsep = 6
while mountinfoarr[optionsep] != "-":
optionsep += 1
return mountinfoarr[optionsep + 2]
def _try_get_emmc_life_time(self, device_name: str) -> float:
# Get eMMC life_time
life_time_path = Path(_BLOCK_DEVICE_EMMC_LIFE_TIME.format(device_name))
if not life_time_path.exists():
return None
# JEDEC health status DEVICE_LIFE_TIME_EST_TYP_A/B
emmc_life_time = life_time_path.read_text().split()
if len(emmc_life_time) < 2:
return None
# Type B life time estimate represents the user partition.
life_time_value = int(emmc_life_time[1], 16)
# 0=Not defined, 1-10=0-100% device life time used, 11=Exceeded
if life_time_value == 0:
return None
if life_time_value == 11:
logging.warning(
"eMMC reports that its estimated life-time has been exceeded!"
)
return 100.0
# Return the pessimistic estimate (0x02 -> 10%-20%, return 20%)
return life_time_value * 10.0
def get_disk_life_time(self, path: Union[str, Path]) -> float:
"""Return life time estimate of the underlying SSD drive."""
mount_source = self._get_mount_source(str(path))
if mount_source == "overlay":
return None
mount_source_path = Path(mount_source)
if not mount_source_path.is_block_device():
return None
# This looks a bit funky but it is more or less what lsblk is doing to get
# the parent dev reliably
# Get class device...
mount_source_device_part = Path(
_BLOCK_DEVICE_CLASS.format(mount_source_path.name)
)
# ... resolve symlink and get parent device from that path.
mount_source_device_name = mount_source_device_part.resolve().parts[-2]
# Currently only eMMC block devices supported
return self._try_get_emmc_life_time(mount_source_device_name)

View File

@@ -72,6 +72,13 @@ class InfoCenter(CoreSysAttributes):
self.coresys.config.path_supervisor
)
@property
def disk_life_time(self) -> float:
"""Return the estimated life-time usage (in %) of the SSD storing the data directory."""
return self.sys_hardware.helper.get_disk_life_time(
self.coresys.config.path_supervisor
)
async def get_dmesg(self) -> bytes:
"""Return host dmesg output."""
proc = await asyncio.create_subprocess_shell(

View File

@@ -101,7 +101,7 @@ SCHEMA_SNAPSHOT = vol.Schema(
{
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_VERSION): version_tag,
vol.Optional(ATTR_SIZE, default=0): vol.Coerce(float),
},
extra=vol.REMOVE_EXTRA,

View File

@@ -1,4 +1,5 @@
"""Test hardware utils."""
# pylint: disable=protected-access
from pathlib import Path
from unittest.mock import MagicMock, patch
@@ -98,3 +99,29 @@ def test_used_space(coresys):
used = coresys.hardware.helper.get_disk_used_space("/data")
assert used == 8.0
def test_get_mountinfo(coresys):
"""Test mountinfo helper."""
mountinfo = coresys.hardware.helper._get_mountinfo("/proc")
assert mountinfo[4] == "/proc"
def test_get_mount_source(coresys):
"""Test mount source helper."""
# For /proc the mount source is known to be "proc"...
mount_source = coresys.hardware.helper._get_mount_source("/proc")
assert mount_source == "proc"
def test_try_get_emmc_life_time(coresys, tmp_path):
"""Test eMMC life time helper."""
fake_life_time = tmp_path / "fake-mmcblk0-lifetime"
fake_life_time.write_text("0x01 0x02\n")
with patch(
"supervisor.hardware.helper._BLOCK_DEVICE_EMMC_LIFE_TIME",
str(tmp_path / "fake-{}-lifetime"),
):
value = coresys.hardware.helper._try_get_emmc_life_time("mmcblk0")
assert value == 20.0