mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-05 09:00:01 +00:00
* Formally deprecate CodeNotary build config * Remove CodeNotary specific integrity checking The current code is specific to how CodeNotary was doing integrity checking. A future integrity checking mechanism likely will work differently (e.g. through EROFS based containers). Remove the current code to make way for a future implementation. * Drop CodeNotary integrity fixups * Drop unused tests * Fix pytest * Fix pytest * Remove CodeNotary related exceptions and handling Remove CodeNotary related exceptions and handling from the Docker interface. * Drop unnecessary comment * Remove Codenotary specific IssueType/SuggestionType * Drop Codenotary specific environment and secret reference * Remove unused constants * Introduce APIGone exception for removed APIs Introduce a new exception class APIGone to indicate that certain API features have been removed and are no longer available. Update the security integrity check endpoint to raise this new exception instead of a generic APIError, providing clearer communication to clients that the feature has been intentionally removed. * Drop content trust A cosign based signature verification will likely be named differently to avoid confusion with existing implementations. For now, remove the content trust option entirely. * Drop code sign test * Remove source_mods/content_trust evaluations * Remove content_trust reference in bootstrap.py * Fix security tests * Drop unused tests * Drop codenotary from schema Since we have "remove extra" in voluptuous, we can remove the codenotary field from the addon schema. * Remove content_trust from tests * Remove content_trust unsupported reason * Remove unnecessary comment * Remove unrelated pytest * Remove unrelated fixtures
244 lines
7.8 KiB
Python
244 lines
7.8 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 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."""
|