1
mirror of https://github.com/home-assistant/supervisor synced 2024-08-02 22:09:57 +02:00
ha-supervisor/supervisor/hassos.py
Pascal Vizeli efcfc1f841
Watchdog for Add-ons (#1970)
* Watchdog for Add-ons

* Run task

* Extend appliaction watchdog

* fix spell

* Add running tasks

* Add tests

* Fix states

* Update supervisor/misc/tasks.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/test_validate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Adjust timeout

* change timeout

* Modify tasker

* slots the task object

* fix typing

* Add tests

* fix lint

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-08-26 22:20:35 +02:00

169 lines
5.4 KiB
Python

"""HassOS support on supervisor."""
import asyncio
import logging
from pathlib import Path
from typing import Awaitable, Optional
import aiohttp
from cpe import CPE
from .const import URL_HASSOS_OTA
from .coresys import CoreSys, CoreSysAttributes
from .dbus.rauc import RaucState
from .exceptions import DBusError, HassOSNotSupportedError, HassOSUpdateError
_LOGGER: logging.Logger = logging.getLogger(__name__)
class HassOS(CoreSysAttributes):
"""HassOS interface inside supervisor."""
def __init__(self, coresys: CoreSys):
"""Initialize HassOS handler."""
self.coresys: CoreSys = coresys
self._available: bool = False
self._version: Optional[str] = None
self._board: Optional[str] = None
@property
def available(self) -> bool:
"""Return True, if HassOS on host."""
return self._available
@property
def version(self) -> Optional[str]:
"""Return version of HassOS."""
return self._version
@property
def latest_version(self) -> str:
"""Return version of HassOS."""
return self.sys_updater.version_hassos
@property
def need_update(self) -> bool:
"""Return true if a HassOS update is available."""
return self.version != self.latest_version
@property
def board(self) -> Optional[str]:
"""Return board name."""
return self._board
def _check_host(self) -> None:
"""Check if HassOS is available."""
if not self.available:
_LOGGER.error("No HassOS available")
raise HassOSNotSupportedError()
async def _download_raucb(self, version: str) -> Path:
"""Download rauc bundle (OTA) from github."""
url = URL_HASSOS_OTA.format(version=version, board=self.board)
raucb = Path(self.sys_config.path_tmp, f"hassos-{version}.raucb")
_LOGGER.info("Fetch OTA update from %s", url)
try:
timeout = aiohttp.ClientTimeout(total=600)
async with self.sys_websession.get(url, timeout=timeout) as request:
if request.status != 200:
raise HassOSUpdateError()
# 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("OTA update is downloaded on %s", raucb)
return raucb
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.warning("Can't fetch versions from %s: %s", url, err)
except OSError as err:
_LOGGER.error("Can't write OTA file: %s", err)
raise HassOSUpdateError()
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)
if cpe.get_product()[0] != "hassos":
raise NotImplementedError()
except NotImplementedError:
_LOGGER.info("No Home Assistant Operating System found")
return
else:
self._available = True
# Store meta data
self._version = cpe.get_version()[0]
self._board = cpe.get_target_hardware()[0]
await self.sys_dbus.rauc.update()
_LOGGER.info(
"Detect HassOS %s / BootSlot %s", self.version, self.sys_dbus.rauc.boot_slot
)
def config_sync(self) -> Awaitable[None]:
"""Trigger a host config reload from usb.
Return a coroutine.
"""
self._check_host()
_LOGGER.info("Syncing configuration from USB with HassOS.")
return self.sys_host.services.restart("hassos-config.service")
async def update(self, version: Optional[str] = None) -> None:
"""Update HassOS system."""
version = version or self.latest_version
# Check installed version
self._check_host()
if version == self.version:
_LOGGER.warning("Version %s is already installed", version)
raise HassOSUpdateError()
# Fetch files from internet
int_ota = await self._download_raucb(version)
ext_ota = Path(self.sys_config.path_extern_tmp, int_ota.name)
try:
await self.sys_dbus.rauc.install(ext_ota)
completed = await self.sys_dbus.rauc.signal_completed()
except DBusError as err:
_LOGGER.error("Rauc communication error")
raise HassOSUpdateError() from err
finally:
int_ota.unlink()
# Update success
if 0 in completed:
_LOGGER.info("Install HassOS %s success", version)
self.sys_create_task(self.sys_host.control.reboot())
return
# Update failed
await self.sys_dbus.rauc.update()
_LOGGER.error("HassOS update failed with: %s", self.sys_dbus.rauc.last_error)
raise HassOSUpdateError()
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 healty!")
else:
_LOGGER.info("Rauc: %s - %s", self.sys_dbus.rauc.boot_slot, response[1])