supervisor/hassio/hassos.py
Pascal Vizeli 1edec61133
Add Ingress support (#991)
* Add Ingress support to supervisor

* Update security

* cleanup add-on extraction

* update description

* fix header part

* fix

* Fix header check

* fix tox

* Migrate docker interface typing

* Update home assistant to new docker

* Migrate supervisor

* Fix host add-on problem

* Update hassos

* Update API

* Expose data to API

* Check on API ingress support

* Add ingress URL

* Some cleanups

* debug

* disable uvloop

* Fix issue

* test

* Fix bug

* Fix flow

* Fix interface

* Fix network

* Fix metadata

* cleanups

* Fix exception

* Migrate to token system

* Fix webui

* Fix update

* Fix relaod

* Update log messages

* Attach ingress url only if enabled

* Cleanup ingress url handling

* Ingress update

* Support check version

* Fix raise error

* Migrate default port

* Fix junks

* search error

* Fix content filter

* Add debug

* Update log

* Update flags

* Update documentation

* Cleanup debugs

* Fix lint

* change default port to 8099

* Fix lint

* fix lint
2019-04-05 12:13:44 +02:00

194 lines
6.1 KiB
Python

"""HassOS support on supervisor."""
import asyncio
from contextlib import suppress
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 CoreSysAttributes, CoreSys
from .docker.hassos_cli import DockerHassOSCli
from .exceptions import (
DBusError,
HassOSNotSupportedError,
HassOSUpdateError,
DockerAPIError,
)
_LOGGER = logging.getLogger(__name__)
class HassOS(CoreSysAttributes):
"""HassOS interface inside HassIO."""
def __init__(self, coresys: CoreSys):
"""Initialize HassOS handler."""
self.coresys: CoreSys = coresys
self.instance: DockerHassOSCli = DockerHassOSCli(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 version_cli(self) -> Optional[str]:
"""Return version of HassOS cli."""
return self.instance.version
@property
def version_latest(self) -> str:
"""Return version of HassOS."""
return self.sys_updater.version_hassos
@property
def version_cli_latest(self) -> str:
"""Return version of HassOS."""
return self.sys_updater.version_hassos_cli
@property
def need_update(self) -> bool:
"""Return true if a HassOS update is available."""
return self.version != self.version_latest
@property
def need_cli_update(self) -> bool:
"""Return true if a HassOS cli update is available."""
return self.version_cli != self.version_cli_latest
@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) -> None:
"""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")
try:
_LOGGER.info("Fetch OTA update from %s", url)
async with self.sys_websession.get(url) 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:
# Check needed host functions
assert self.sys_dbus.rauc.is_connected
assert self.sys_dbus.systemd.is_connected
assert self.sys_dbus.hostname.is_connected
assert self.sys_host.info.cpe is not None
cpe = CPE(self.sys_host.info.cpe)
assert cpe.get_product()[0] == "hassos"
except (AssertionError, NotImplementedError):
_LOGGER.debug("Found no HassOS")
return
# Store meta data
self._available = True
self._version = cpe.get_version()[0]
self._board = cpe.get_target_hardware()[0]
_LOGGER.info("Detect HassOS %s on host system", self.version)
with suppress(DockerAPIError):
await self.instance.attach()
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.version_latest
# 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:
_LOGGER.error("Rauc communication error")
raise HassOSUpdateError() from None
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 fails
rauc_status = await self.sys_dbus.get_properties()
_LOGGER.error("HassOS update fails with: %s", rauc_status.get("LastError"))
raise HassOSUpdateError()
async def update_cli(self, version: Optional[str] = None) -> None:
"""Update local HassOS cli."""
version = version or self.version_cli_latest
if version == self.version_cli:
_LOGGER.warning("Version %s is already installed for CLI", version)
return
try:
await self.instance.update(version)
except DockerAPIError:
_LOGGER.error("HassOS CLI update fails")
raise HassOSUpdateError() from None