mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-20 08:20:15 +00:00
* Add mypy to ci and precommit * Run precommit mypy in venv * Fix issues raised in latest version of mypy
251 lines
7.9 KiB
Python
251 lines
7.9 KiB
Python
"""Supervisor plugins base class."""
|
|
|
|
from abc import ABC, abstractmethod
|
|
import asyncio
|
|
from collections.abc import Awaitable
|
|
from contextlib import suppress
|
|
import logging
|
|
|
|
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
|
|
|
from ..const import ATTR_IMAGE, ATTR_VERSION, BusEvent
|
|
from ..coresys import CoreSysAttributes
|
|
from ..docker.const import ContainerState
|
|
from ..docker.interface import DockerInterface
|
|
from ..docker.monitor import DockerContainerStateEvent
|
|
from ..exceptions import DockerError, PluginError
|
|
from ..utils.common import FileConfiguration
|
|
from ..utils.sentry import async_capture_exception
|
|
from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS
|
|
|
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
|
"""Base class for plugins."""
|
|
|
|
slug: str
|
|
instance: DockerInterface
|
|
|
|
@property
|
|
def version(self) -> AwesomeVersion | None:
|
|
"""Return current version of the plugin."""
|
|
return self._data.get(ATTR_VERSION)
|
|
|
|
@version.setter
|
|
def version(self, value: AwesomeVersion) -> None:
|
|
"""Set current version of the plugin."""
|
|
self._data[ATTR_VERSION] = value
|
|
|
|
@property
|
|
def default_image(self) -> str:
|
|
"""Return default image for plugin."""
|
|
return f"ghcr.io/home-assistant/{self.sys_arch.supervisor}-hassio-{self.slug}"
|
|
|
|
@property
|
|
def image(self) -> str:
|
|
"""Return current image of plugin."""
|
|
if self._data.get(ATTR_IMAGE):
|
|
return self._data[ATTR_IMAGE]
|
|
return self.default_image
|
|
|
|
@image.setter
|
|
def image(self, value: str) -> None:
|
|
"""Return current image of the plugin."""
|
|
self._data[ATTR_IMAGE] = value
|
|
|
|
@property
|
|
@abstractmethod
|
|
def latest_version(self) -> AwesomeVersion | None:
|
|
"""Return latest version of the plugin."""
|
|
|
|
@property
|
|
def need_update(self) -> bool:
|
|
"""Return True if an update is available."""
|
|
try:
|
|
return (
|
|
self.version is not None
|
|
and self.latest_version is not None
|
|
and self.version < self.latest_version
|
|
)
|
|
except (AwesomeVersionException, TypeError):
|
|
return False
|
|
|
|
@property
|
|
def in_progress(self) -> bool:
|
|
"""Return True if a task is in progress."""
|
|
return self.instance.in_progress
|
|
|
|
def check_trust(self) -> Awaitable[None]:
|
|
"""Calculate plugin docker content trust.
|
|
|
|
Return Coroutine.
|
|
"""
|
|
return self.instance.check_trust()
|
|
|
|
def logs(self) -> Awaitable[bytes]:
|
|
"""Get docker plugin logs.
|
|
|
|
Return Coroutine.
|
|
"""
|
|
return self.instance.logs()
|
|
|
|
def is_running(self) -> Awaitable[bool]:
|
|
"""Return True if Docker container is running.
|
|
|
|
Return a coroutine.
|
|
"""
|
|
return self.instance.is_running()
|
|
|
|
def is_failed(self) -> Awaitable[bool]:
|
|
"""Return True if a Docker container is failed state.
|
|
|
|
Return a coroutine.
|
|
"""
|
|
return self.instance.is_failed()
|
|
|
|
def start_watchdog(self) -> None:
|
|
"""Register docker container listener for plugin."""
|
|
self.sys_bus.register_event(
|
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container
|
|
)
|
|
|
|
async def watchdog_container(self, event: DockerContainerStateEvent) -> None:
|
|
"""Process state changes in plugin container and restart if necessary."""
|
|
if event.name != self.instance.name:
|
|
return
|
|
|
|
if event.state in {ContainerState.FAILED, ContainerState.UNHEALTHY}:
|
|
await self._restart_after_problem(event.state)
|
|
|
|
async def _restart_after_problem(self, state: ContainerState):
|
|
"""Restart unhealthy or failed plugin."""
|
|
attempts = 0
|
|
while await self.instance.current_state() == state:
|
|
if not self.in_progress:
|
|
_LOGGER.warning(
|
|
"Watchdog found %s plugin %s, restarting...",
|
|
self.slug,
|
|
state,
|
|
)
|
|
try:
|
|
await self.rebuild()
|
|
except PluginError as err:
|
|
attempts = attempts + 1
|
|
_LOGGER.error("Watchdog restart of %s plugin failed!", self.slug)
|
|
await async_capture_exception(err)
|
|
else:
|
|
break
|
|
|
|
if attempts >= WATCHDOG_MAX_ATTEMPTS:
|
|
_LOGGER.critical(
|
|
"Watchdog cannot restart %s plugin, failed all %s attempts",
|
|
self.slug,
|
|
attempts,
|
|
)
|
|
break
|
|
|
|
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
|
|
|
async def rebuild(self) -> None:
|
|
"""Rebuild system plugin."""
|
|
with suppress(DockerError):
|
|
await self.instance.stop()
|
|
await self.start()
|
|
|
|
@abstractmethod
|
|
async def start(self) -> None:
|
|
"""Start system plugin."""
|
|
|
|
@abstractmethod
|
|
async def stop(self) -> None:
|
|
"""Stop system plugin."""
|
|
|
|
async def load(self) -> None:
|
|
"""Load system plugin."""
|
|
self.start_watchdog()
|
|
|
|
# Check plugin state
|
|
try:
|
|
# Evaluate Version if we lost this information
|
|
if self.version:
|
|
version = self.version
|
|
else:
|
|
self.version = version = await self.instance.get_latest_version()
|
|
|
|
await self.instance.attach(version=version, skip_state_event_if_down=True)
|
|
|
|
await self.instance.check_image(version, self.default_image)
|
|
except DockerError:
|
|
_LOGGER.info(
|
|
"No %s plugin Docker image %s found.", self.slug, self.instance.image
|
|
)
|
|
|
|
# Install plugin
|
|
with suppress(PluginError):
|
|
await self.install()
|
|
else:
|
|
self.version = self.instance.version or version
|
|
self.image = self.default_image
|
|
await self.save_data()
|
|
|
|
# Run plugin
|
|
with suppress(PluginError):
|
|
if not await self.instance.is_running():
|
|
await self.start()
|
|
|
|
async def install(self) -> None:
|
|
"""Install system plugin."""
|
|
_LOGGER.info("Setup %s plugin", self.slug)
|
|
while True:
|
|
# read plugin tag and install it
|
|
if not self.latest_version:
|
|
await self.sys_updater.reload()
|
|
|
|
if to_version := self.latest_version:
|
|
with suppress(DockerError):
|
|
await self.instance.install(to_version, image=self.default_image)
|
|
self.version = self.instance.version or to_version
|
|
break
|
|
_LOGGER.warning(
|
|
"Error on installing %s plugin, retrying in 30sec", self.slug
|
|
)
|
|
await asyncio.sleep(30)
|
|
|
|
_LOGGER.info("%s plugin now installed", self.slug)
|
|
self.image = self.default_image
|
|
await self.save_data()
|
|
|
|
async def update(self, version: str | None = None) -> None:
|
|
"""Update system plugin."""
|
|
to_version = AwesomeVersion(version) if version else self.latest_version
|
|
if not to_version:
|
|
raise PluginError(
|
|
f"Cannot determine latest version of plugin {self.slug} for update",
|
|
_LOGGER.error,
|
|
)
|
|
|
|
old_image = self.image
|
|
|
|
if to_version == self.version:
|
|
_LOGGER.warning(
|
|
"Version %s is already installed for %s", to_version, self.slug
|
|
)
|
|
return
|
|
|
|
await self.instance.update(to_version, image=self.default_image)
|
|
self.version = self.instance.version or to_version
|
|
self.image = self.default_image
|
|
await self.save_data()
|
|
|
|
# Cleanup
|
|
with suppress(DockerError):
|
|
await self.instance.cleanup(old_image=old_image)
|
|
|
|
# Start plugin
|
|
await self.start()
|
|
|
|
@abstractmethod
|
|
async def repair(self) -> None:
|
|
"""Repair system plugin."""
|