mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-09 10:59:43 +00:00
* Bad message error marks system as unhealthy * Finish adding test cases for changes * Rename test file for uniqueness * bad_message to oserror_bad_message * Omit some checks and check for network mounts
242 lines
8.1 KiB
Python
242 lines
8.1 KiB
Python
"""OS support on supervisor."""
|
|
from collections.abc import Awaitable
|
|
import errno
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import aiohttp
|
|
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
|
from cpe import CPE
|
|
|
|
from ..coresys import CoreSys, CoreSysAttributes
|
|
from ..dbus.agent.boards.const import BOARD_NAME_SUPERVISED
|
|
from ..dbus.rauc import RaucState
|
|
from ..exceptions import DBusError, HassOSJobError, HassOSUpdateError
|
|
from ..jobs.const import JobCondition, JobExecutionLimit
|
|
from ..jobs.decorator import Job
|
|
from ..resolution.const import UnhealthyReason
|
|
from .data_disk import DataDisk
|
|
|
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OSManager(CoreSysAttributes):
|
|
"""OS interface inside supervisor."""
|
|
|
|
def __init__(self, coresys: CoreSys):
|
|
"""Initialize HassOS handler."""
|
|
self.coresys: CoreSys = coresys
|
|
self._datadisk: DataDisk = DataDisk(coresys)
|
|
self._available: bool = False
|
|
self._version: AwesomeVersion | None = None
|
|
self._board: str | None = None
|
|
self._os_name: str | None = None
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return True, if HassOS on host."""
|
|
return self._available
|
|
|
|
@property
|
|
def version(self) -> AwesomeVersion | None:
|
|
"""Return version of HassOS."""
|
|
return self._version
|
|
|
|
@property
|
|
def latest_version(self) -> AwesomeVersion | None:
|
|
"""Return version of HassOS."""
|
|
return self.sys_updater.version_hassos
|
|
|
|
@property
|
|
def need_update(self) -> bool:
|
|
"""Return true if a HassOS update is available."""
|
|
try:
|
|
return self.version < self.latest_version
|
|
except (AwesomeVersionException, TypeError):
|
|
return False
|
|
|
|
@property
|
|
def board(self) -> str | None:
|
|
"""Return board name."""
|
|
return self._board
|
|
|
|
@property
|
|
def os_name(self) -> str | None:
|
|
"""Return OS name."""
|
|
return self._os_name
|
|
|
|
@property
|
|
def datadisk(self) -> DataDisk:
|
|
"""Return Operating-System datadisk."""
|
|
return self._datadisk
|
|
|
|
def _get_download_url(self, version: AwesomeVersion) -> str:
|
|
raw_url = self.sys_updater.ota_url
|
|
if raw_url is None:
|
|
raise HassOSUpdateError("Don't have an URL for OTA updates!", _LOGGER.error)
|
|
|
|
update_board = self.board
|
|
update_os_name = self.os_name
|
|
|
|
# OS version 6 and later renamed intel-nuc to generic-x86-64...
|
|
if update_board == "intel-nuc" and version >= 6.0:
|
|
update_board = "generic-x86-64"
|
|
|
|
# The OS name used to be hassos before renaming to haos...
|
|
if version < 6.0:
|
|
update_os_name = "hassos"
|
|
else:
|
|
update_os_name = "haos"
|
|
|
|
url = raw_url.format(
|
|
version=str(version), board=update_board, os_name=update_os_name
|
|
)
|
|
return url
|
|
|
|
async def _download_raucb(self, url: str, raucb: Path) -> None:
|
|
"""Download rauc bundle (OTA) from URL."""
|
|
_LOGGER.info("Fetch OTA update from %s", url)
|
|
try:
|
|
timeout = aiohttp.ClientTimeout(total=60 * 60, connect=180)
|
|
async with self.sys_websession.get(url, timeout=timeout) as request:
|
|
if request.status != 200:
|
|
raise HassOSUpdateError(
|
|
f"Error raised from OTA Webserver: {request.status}",
|
|
_LOGGER.error,
|
|
)
|
|
|
|
# Download RAUCB file
|
|
with raucb.open("wb") as ota_file:
|
|
while True:
|
|
chunk = await request.content.read(1_048_576)
|
|
if not chunk:
|
|
break
|
|
ota_file.write(chunk)
|
|
|
|
_LOGGER.info("Completed download of OTA update file %s", raucb)
|
|
|
|
except (aiohttp.ClientError, TimeoutError) as err:
|
|
self.sys_supervisor.connectivity = False
|
|
raise HassOSUpdateError(
|
|
f"Can't fetch OTA update from {url}: {err!s}", _LOGGER.error
|
|
) from err
|
|
|
|
except OSError as err:
|
|
if err.errno == errno.EBADMSG:
|
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
|
raise HassOSUpdateError(
|
|
f"Can't write OTA file: {err!s}", _LOGGER.error
|
|
) from err
|
|
|
|
async def load(self) -> None:
|
|
"""Load HassOS data."""
|
|
try:
|
|
if not self.sys_host.info.cpe:
|
|
raise NotImplementedError()
|
|
|
|
cpe = CPE(self.sys_host.info.cpe)
|
|
os_name = cpe.get_product()[0]
|
|
if os_name not in ("hassos", "haos"):
|
|
self._board = BOARD_NAME_SUPERVISED.lower()
|
|
raise NotImplementedError()
|
|
except NotImplementedError:
|
|
_LOGGER.info("No Home Assistant Operating System found")
|
|
return
|
|
|
|
# Store meta data
|
|
self._available = True
|
|
self.sys_host.supported_features.cache_clear()
|
|
self._version = AwesomeVersion(cpe.get_version()[0])
|
|
self._board = cpe.get_target_hardware()[0]
|
|
self._os_name = cpe.get_product()[0]
|
|
|
|
await self.datadisk.load()
|
|
|
|
_LOGGER.info(
|
|
"Detect Home Assistant Operating System %s / BootSlot %s",
|
|
self.version,
|
|
self.sys_dbus.rauc.boot_slot,
|
|
)
|
|
|
|
@Job(
|
|
name="os_manager_config_sync",
|
|
conditions=[JobCondition.HAOS],
|
|
on_condition=HassOSJobError,
|
|
)
|
|
async def config_sync(self) -> Awaitable[None]:
|
|
"""Trigger a host config reload from usb.
|
|
|
|
Return a coroutine.
|
|
"""
|
|
_LOGGER.info(
|
|
"Synchronizing configuration from USB with Home Assistant Operating System."
|
|
)
|
|
await self.sys_host.services.restart("hassos-config.service")
|
|
|
|
@Job(
|
|
name="os_manager_update",
|
|
conditions=[
|
|
JobCondition.HAOS,
|
|
JobCondition.INTERNET_SYSTEM,
|
|
JobCondition.RUNNING,
|
|
JobCondition.SUPERVISOR_UPDATED,
|
|
],
|
|
limit=JobExecutionLimit.ONCE,
|
|
on_condition=HassOSJobError,
|
|
)
|
|
async def update(self, version: AwesomeVersion | None = None) -> None:
|
|
"""Update HassOS system."""
|
|
version = version or self.latest_version
|
|
|
|
# Check installed version
|
|
if version == self.version:
|
|
raise HassOSUpdateError(
|
|
f"Version {version!s} is already installed", _LOGGER.warning
|
|
)
|
|
|
|
# Fetch files from internet
|
|
ota_url = self._get_download_url(version)
|
|
int_ota = Path(self.sys_config.path_tmp, f"hassos-{version!s}.raucb")
|
|
await self._download_raucb(ota_url, int_ota)
|
|
ext_ota = Path(self.sys_config.path_extern_tmp, int_ota.name)
|
|
|
|
try:
|
|
async with self.sys_dbus.rauc.signal_completed() as signal:
|
|
# Start listening for signals before triggering install
|
|
# This prevents a race condition with install complete signal
|
|
|
|
await self.sys_dbus.rauc.install(ext_ota)
|
|
completed = await signal.wait_for_signal()
|
|
|
|
except DBusError as err:
|
|
raise HassOSUpdateError("Rauc communication error", _LOGGER.error) from err
|
|
|
|
finally:
|
|
int_ota.unlink()
|
|
|
|
# Update success
|
|
if 0 in completed:
|
|
_LOGGER.info(
|
|
"Install of Home Assistant Operating System %s success", version
|
|
)
|
|
self.sys_create_task(self.sys_host.control.reboot())
|
|
return
|
|
|
|
# Update failed
|
|
await self.sys_dbus.rauc.update()
|
|
_LOGGER.error(
|
|
"Home Assistant Operating System update failed with: %s",
|
|
self.sys_dbus.rauc.last_error,
|
|
)
|
|
raise HassOSUpdateError()
|
|
|
|
@Job(name="os_manager_mark_healthy", conditions=[JobCondition.HAOS], internal=True)
|
|
async def mark_healthy(self) -> None:
|
|
"""Set booted partition as good for rauc."""
|
|
try:
|
|
response = await self.sys_dbus.rauc.mark(RaucState.GOOD, "booted")
|
|
except DBusError:
|
|
_LOGGER.error("Can't mark booted partition as healthy!")
|
|
else:
|
|
_LOGGER.info("Rauc: %s - %s", self.sys_dbus.rauc.boot_slot, response[1])
|