From f0ed2eba2bfbe1b735f4c5c0c45992ef8726273e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 5 Apr 2020 23:26:22 +0200 Subject: [PATCH] Multicast support on Hass.io Network (#1634) * Add multicast layer to docker * support network forward * add pluginmanager * finish multicast plugin * fix lint * Add shutdown for plugins * Add API * Add watchdog * Fix black * Fix path --- API.md | 36 +++++ supervisor/addons/__init__.py | 4 +- supervisor/addons/addon.py | 2 +- supervisor/api/__init__.py | 16 +++ supervisor/api/audio.py | 16 +-- supervisor/api/cli.py | 10 +- supervisor/api/dns.py | 26 ++-- supervisor/api/multicast.py | 76 +++++++++++ supervisor/api/security.py | 2 +- supervisor/bootstrap.py | 8 +- supervisor/const.py | 3 + supervisor/core.py | 13 +- supervisor/coresys.py | 66 ++------- supervisor/docker/addon.py | 8 +- supervisor/docker/audio.py | 4 +- supervisor/docker/cli.py | 6 +- supervisor/docker/dns.py | 4 +- supervisor/docker/homeassistant.py | 4 +- supervisor/docker/multicast.py | 59 ++++++++ supervisor/exceptions.py | 13 +- supervisor/homeassistant.py | 2 +- supervisor/plugins/__init__.py | 75 +++++++++++ supervisor/plugins/audio.py | 21 ++- supervisor/plugins/cli.py | 16 ++- supervisor/plugins/dns.py | 27 +++- supervisor/plugins/multicast.py | 208 +++++++++++++++++++++++++++++ supervisor/plugins/validate.py | 44 ++++++ supervisor/tasks.py | 76 ++++++++--- supervisor/updater.py | 21 ++- supervisor/validate.py | 35 +---- 30 files changed, 730 insertions(+), 171 deletions(-) create mode 100644 supervisor/api/multicast.py create mode 100644 supervisor/docker/multicast.py create mode 100644 supervisor/plugins/multicast.py create mode 100644 supervisor/plugins/validate.py diff --git a/API.md b/API.md index 305417f35..252ab59fc 100644 --- a/API.md +++ b/API.md @@ -935,6 +935,42 @@ return: } ``` +### Multicast + +- GET `/multicast/info` + +```json +{ + "version": "1", + "version_latest": "2" +} +``` + +- POST `/multicast/update` + +```json +{ + "version": "VERSION" +} +``` + +- POST `/multicast/restart` + +- GET `/multicast/stats` + +```json +{ + "cpu_percent": 0.0, + "memory_usage": 283123, + "memory_limit": 329392, + "memory_percent": 1.4, + "network_tx": 0, + "network_rx": 0, + "blk_read": 0, + "blk_write": 0 +} +``` + ### Auth / SSO API You can use the user system on homeassistant. We handle this auth system on diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index 865f66b8e..3d33a7c7c 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -325,10 +325,10 @@ class AddonManager(CoreSysAttributes): for addon in self.installed: if not await addon.instance.is_running(): continue - self.sys_dns.add_host( + self.sys_plugins.dns.add_host( ipv4=addon.ip_address, names=[addon.hostname], write=False ) # Write hosts files with suppress(CoreDNSError): - self.sys_dns.write_hosts() + self.sys_plugins.dns.write_hosts() diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 38c6b4ad4..1342831f8 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -389,7 +389,7 @@ class Addon(AddonModel): def write_pulse(self): """Write asound config to file and return True on success.""" - pulse_config = self.sys_audio.pulse_client( + pulse_config = self.sys_plugins.audio.pulse_client( input_profile=self.audio_input, output_profile=self.audio_output ) diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index e405f88e6..2d0643a9c 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -23,6 +23,7 @@ from .security import SecurityMiddleware from .services import APIServices from .snapshots import APISnapshots from .supervisor import APISupervisor +from .multicast import APIMulticast _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -52,6 +53,7 @@ class RestAPI(CoreSysAttributes): self._register_host() self._register_os() self._register_cli() + self._register_multicast() self._register_hardware() self._register_homeassistant() self._register_proxy() @@ -113,6 +115,20 @@ class RestAPI(CoreSysAttributes): ] ) + def _register_multicast(self) -> None: + """Register Multicast functions.""" + api_multicast = APIMulticast() + api_multicast.coresys = self.coresys + + self.webapp.add_routes( + [ + web.get("/multicast/info", api_multicast.info), + web.get("/multicast/stats", api_multicast.stats), + web.post("/multicast/update", api_multicast.update), + web.post("/multicast/restart", api_multicast.restart), + ] + ) + def _register_hardware(self) -> None: """Register hardware functions.""" api_hardware = APIHardware() diff --git a/supervisor/api/audio.py b/supervisor/api/audio.py index 7a1cada11..c8e66d60a 100644 --- a/supervisor/api/audio.py +++ b/supervisor/api/audio.py @@ -68,8 +68,8 @@ class APIAudio(CoreSysAttributes): async def info(self, request: web.Request) -> Dict[str, Any]: """Return Audio information.""" return { - ATTR_VERSION: self.sys_audio.version, - ATTR_VERSION_LATEST: self.sys_audio.latest_version, + ATTR_VERSION: self.sys_plugins.audio.version, + ATTR_VERSION_LATEST: self.sys_plugins.audio.latest_version, ATTR_HOST: str(self.sys_docker.network.audio), ATTR_AUDIO: { ATTR_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards], @@ -88,7 +88,7 @@ class APIAudio(CoreSysAttributes): @api_process async def stats(self, request: web.Request) -> Dict[str, Any]: """Return resource information.""" - stats = await self.sys_audio.stats() + stats = await self.sys_plugins.audio.stats() return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -105,21 +105,21 @@ class APIAudio(CoreSysAttributes): async def update(self, request: web.Request) -> None: """Update Audio plugin.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.sys_audio.latest_version) + version = body.get(ATTR_VERSION, self.sys_plugins.audio.latest_version) - if version == self.sys_audio.version: + if version == self.sys_plugins.audio.version: raise APIError("Version {} is already in use".format(version)) - await asyncio.shield(self.sys_audio.update(version)) + await asyncio.shield(self.sys_plugins.audio.update(version)) @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request: web.Request) -> Awaitable[bytes]: """Return Audio Docker logs.""" - return self.sys_audio.logs() + return self.sys_plugins.audio.logs() @api_process def restart(self, request: web.Request) -> Awaitable[None]: """Restart Audio plugin.""" - return asyncio.shield(self.sys_audio.restart()) + return asyncio.shield(self.sys_plugins.audio.restart()) @api_process def reload(self, request: web.Request) -> Awaitable[None]: diff --git a/supervisor/api/cli.py b/supervisor/api/cli.py index 34cf13545..5a1aba8d5 100644 --- a/supervisor/api/cli.py +++ b/supervisor/api/cli.py @@ -33,14 +33,14 @@ class APICli(CoreSysAttributes): async def info(self, request: web.Request) -> Dict[str, Any]: """Return HA cli information.""" return { - ATTR_VERSION: self.sys_cli.version, - ATTR_VERSION_LATEST: self.sys_cli.latest_version, + ATTR_VERSION: self.sys_plugins.cli.version, + ATTR_VERSION_LATEST: self.sys_plugins.cli.latest_version, } @api_process async def stats(self, request: web.Request) -> Dict[str, Any]: """Return resource information.""" - stats = await self.sys_cli.stats() + stats = await self.sys_plugins.cli.stats() return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -57,6 +57,6 @@ class APICli(CoreSysAttributes): async def update(self, request: web.Request) -> None: """Update HA CLI.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.sys_cli.latest_version) + version = body.get(ATTR_VERSION, self.sys_plugins.cli.latest_version) - await asyncio.shield(self.sys_cli.update(version)) + await asyncio.shield(self.sys_plugins.cli.update(version)) diff --git a/supervisor/api/dns.py b/supervisor/api/dns.py index f834b4327..80e7a9c59 100644 --- a/supervisor/api/dns.py +++ b/supervisor/api/dns.py @@ -42,10 +42,10 @@ class APICoreDNS(CoreSysAttributes): async def info(self, request: web.Request) -> Dict[str, Any]: """Return DNS information.""" return { - ATTR_VERSION: self.sys_dns.version, - ATTR_VERSION_LATEST: self.sys_dns.latest_version, + ATTR_VERSION: self.sys_plugins.dns.version, + ATTR_VERSION_LATEST: self.sys_plugins.dns.latest_version, ATTR_HOST: str(self.sys_docker.network.dns), - ATTR_SERVERS: self.sys_dns.servers, + ATTR_SERVERS: self.sys_plugins.dns.servers, ATTR_LOCALS: self.sys_host.network.dns_servers, } @@ -55,15 +55,15 @@ class APICoreDNS(CoreSysAttributes): body = await api_validate(SCHEMA_OPTIONS, request) if ATTR_SERVERS in body: - self.sys_dns.servers = body[ATTR_SERVERS] - self.sys_create_task(self.sys_dns.restart()) + self.sys_plugins.dns.servers = body[ATTR_SERVERS] + self.sys_create_task(self.sys_plugins.dns.restart()) - self.sys_dns.save_data() + self.sys_plugins.dns.save_data() @api_process async def stats(self, request: web.Request) -> Dict[str, Any]: """Return resource information.""" - stats = await self.sys_dns.stats() + stats = await self.sys_plugins.dns.stats() return { ATTR_CPU_PERCENT: stats.cpu_percent, @@ -80,23 +80,23 @@ class APICoreDNS(CoreSysAttributes): async def update(self, request: web.Request) -> None: """Update DNS plugin.""" body = await api_validate(SCHEMA_VERSION, request) - version = body.get(ATTR_VERSION, self.sys_dns.latest_version) + version = body.get(ATTR_VERSION, self.sys_plugins.dns.latest_version) - if version == self.sys_dns.version: + if version == self.sys_plugins.dns.version: raise APIError("Version {} is already in use".format(version)) - await asyncio.shield(self.sys_dns.update(version)) + await asyncio.shield(self.sys_plugins.dns.update(version)) @api_process_raw(CONTENT_TYPE_BINARY) def logs(self, request: web.Request) -> Awaitable[bytes]: """Return DNS Docker logs.""" - return self.sys_dns.logs() + return self.sys_plugins.dns.logs() @api_process def restart(self, request: web.Request) -> Awaitable[None]: """Restart CoreDNS plugin.""" - return asyncio.shield(self.sys_dns.restart()) + return asyncio.shield(self.sys_plugins.dns.restart()) @api_process def reset(self, request: web.Request) -> Awaitable[None]: """Reset CoreDNS plugin.""" - return asyncio.shield(self.sys_dns.reset()) + return asyncio.shield(self.sys_plugins.dns.reset()) diff --git a/supervisor/api/multicast.py b/supervisor/api/multicast.py new file mode 100644 index 000000000..1b3c33b28 --- /dev/null +++ b/supervisor/api/multicast.py @@ -0,0 +1,76 @@ +"""Init file for Supervisor Multicast RESTful API.""" +import asyncio +import logging +from typing import Any, Awaitable, Dict + +from aiohttp import web +import voluptuous as vol + +from ..const import ( + ATTR_BLK_READ, + ATTR_BLK_WRITE, + ATTR_CPU_PERCENT, + ATTR_VERSION_LATEST, + ATTR_MEMORY_LIMIT, + ATTR_MEMORY_PERCENT, + ATTR_MEMORY_USAGE, + ATTR_NETWORK_RX, + ATTR_NETWORK_TX, + ATTR_VERSION, + CONTENT_TYPE_BINARY, +) +from ..coresys import CoreSysAttributes +from ..exceptions import APIError +from .utils import api_process, api_process_raw, api_validate + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)}) + + +class APIMulticast(CoreSysAttributes): + """Handle RESTful API for Multicast functions.""" + + @api_process + async def info(self, request: web.Request) -> Dict[str, Any]: + """Return Multicast information.""" + return { + ATTR_VERSION: self.sys_plugins.multicast.version, + ATTR_VERSION_LATEST: self.sys_plugins.multicast.latest_version, + } + + @api_process + async def stats(self, request: web.Request) -> Dict[str, Any]: + """Return resource information.""" + stats = await self.sys_plugins.multicast.stats() + + return { + ATTR_CPU_PERCENT: stats.cpu_percent, + ATTR_MEMORY_USAGE: stats.memory_usage, + ATTR_MEMORY_LIMIT: stats.memory_limit, + ATTR_MEMORY_PERCENT: stats.memory_percent, + ATTR_NETWORK_RX: stats.network_rx, + ATTR_NETWORK_TX: stats.network_tx, + ATTR_BLK_READ: stats.blk_read, + ATTR_BLK_WRITE: stats.blk_write, + } + + @api_process + async def update(self, request: web.Request) -> None: + """Update Multicast plugin.""" + body = await api_validate(SCHEMA_VERSION, request) + version = body.get(ATTR_VERSION, self.sys_plugins.multicast.latest_version) + + if version == self.sys_plugins.multicast.version: + raise APIError("Version {} is already in use".format(version)) + await asyncio.shield(self.sys_plugins.multicast.update(version)) + + @api_process_raw(CONTENT_TYPE_BINARY) + def logs(self, request: web.Request) -> Awaitable[bytes]: + """Return Multicast Docker logs.""" + return self.sys_plugins.multicast.logs() + + @api_process + def restart(self, request: web.Request) -> Awaitable[None]: + """Restart Multicast plugin.""" + return asyncio.shield(self.sys_plugins.multicast.restart()) diff --git a/supervisor/api/security.py b/supervisor/api/security.py index 663c4b062..559104c43 100644 --- a/supervisor/api/security.py +++ b/supervisor/api/security.py @@ -130,7 +130,7 @@ class SecurityMiddleware(CoreSysAttributes): # Host # Remove machine_id handling later if all use new CLI - if supervisor_token in (self.sys_machine_id, self.sys_cli.supervisor_token): + if supervisor_token in (self.sys_machine_id, self.sys_plugins.cli.supervisor_token): _LOGGER.debug("%s access from Host", request.path) request_from = self.sys_host diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 2e49ff451..932180ca4 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -35,10 +35,8 @@ from .supervisor import Supervisor from .tasks import Tasks from .updater import Updater from .secrets import SecretsManager +from .plugins import PluginManager from .utils.dt import fetch_timezone -from .plugins.dns import CoreDNS -from .plugins.cli import HaCli -from .plugins.audio import Audio _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -52,9 +50,8 @@ async def initialize_coresys(): # Initialize core objects coresys.core = Core(coresys) - coresys.dns = CoreDNS(coresys) + coresys.plugins = PluginManager(coresys) coresys.arch = CpuArch(coresys) - coresys.audio = Audio(coresys) coresys.auth = Auth(coresys) coresys.updater = Updater(coresys) coresys.api = RestAPI(coresys) @@ -72,7 +69,6 @@ async def initialize_coresys(): coresys.dbus = DBusManager(coresys) coresys.hassos = HassOS(coresys) coresys.secrets = SecretsManager(coresys) - coresys.cli = HaCli(coresys) # bootstrap config initialize_system_data(coresys) diff --git a/supervisor/const.py b/supervisor/const.py index ce64a018f..7bb025e8e 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -28,6 +28,7 @@ FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json") FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json") FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json") FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json") +FILE_HASSIO_MULTICAST = Path(SUPERVISOR_DATA, "multicast.json") SOCKET_DOCKER = Path("/run/docker.sock") @@ -67,6 +68,7 @@ HEADER_TOKEN_OLD = "X-Hassio-Key" ENV_TOKEN_OLD = "HASSIO_TOKEN" ENV_TOKEN = "SUPERVISOR_TOKEN" ENV_TIME = "TZ" +ENV_HASSIO_NETWORK = "HASSIO_NETWORK" ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY" ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE" @@ -77,6 +79,7 @@ REQUEST_FROM = "HASSIO_FROM" ATTR_SUPERVISOR = "supervisor" ATTR_MACHINE = "machine" +ATTR_MULTICAST = "multicast" ATTR_WAIT_BOOT = "wait_boot" ATTR_DEPLOYMENT = "deployment" ATTR_WATCHDOG = "watchdog" diff --git a/supervisor/core.py b/supervisor/core.py index e73938c45..a2f22cfd6 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -41,9 +41,7 @@ class Core(CoreSysAttributes): await self.sys_host.load() # Load Plugins container - await asyncio.wait( - [self.sys_dns.load(), self.sys_audio.load(), self.sys_cli.load()] - ) + await self.sys_plugins.load() # Load Home Assistant await self.sys_homeassistant.load() @@ -172,7 +170,7 @@ class Core(CoreSysAttributes): self.sys_websession.close(), self.sys_websession_ssl.close(), self.sys_ingress.unload(), - self.sys_dns.unload(), + self.sys_plugins.unload(), self.sys_hwmonitor.unload(), ] ) @@ -193,6 +191,9 @@ class Core(CoreSysAttributes): await self.sys_addons.shutdown(STARTUP_SYSTEM) await self.sys_addons.shutdown(STARTUP_INITIALIZE) + # Shutdown all Plugins + await self.sys_plugins.shutdown() + def _update_last_boot(self): """Update last boot time.""" self.sys_config.last_boot = self.sys_hardware.last_boot @@ -204,9 +205,7 @@ class Core(CoreSysAttributes): await self.sys_run_in_executor(self.sys_docker.repair) # Fix plugins - await asyncio.wait( - [self.sys_dns.repair(), self.sys_audio.repair(), self.sys_cli.repair()] - ) + await self.sys_plugins.repair() # Restore core functionality await self.sys_addons.repair() diff --git a/supervisor/coresys.py b/supervisor/coresys.py index b80c069e5..f04c200fb 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -31,9 +31,7 @@ if TYPE_CHECKING: from .store import StoreManager from .tasks import Tasks from .updater import Updater - from .plugins.cli import HaCli - from .plugins.audio import Audio - from .plugins.dns import CoreDNS + from .plugins import PluginManager class CoreSys: @@ -61,10 +59,7 @@ class CoreSys: # Internal objects pointers self._core: Optional[Core] = None self._arch: Optional[CpuArch] = None - self._audio: Optional[Audio] = None self._auth: Optional[Auth] = None - self._dns: Optional[CoreDNS] = None - self._cli: Optional[HaCli] = None self._homeassistant: Optional[HomeAssistant] = None self._supervisor: Optional[Supervisor] = None self._addons: Optional[AddonManager] = None @@ -81,6 +76,7 @@ class CoreSys: self._store: Optional[StoreManager] = None self._discovery: Optional[Discovery] = None self._hwmonitor: Optional[HwMonitor] = None + self._plugins: Optional[PluginManager] = None @property def dev(self) -> bool: @@ -140,16 +136,16 @@ class CoreSys: self._core = value @property - def cli(self) -> HaCli: - """Return HaCli object.""" - return self._cli + def plugins(self) -> PluginManager: + """Return PluginManager object.""" + return self._plugins - @cli.setter - def cli(self, value: HaCli): - """Set a HaCli object.""" - if self._cli: - raise RuntimeError("HaCli already set!") - self._cli = value + @plugins.setter + def plugins(self, value: PluginManager): + """Set a PluginManager object.""" + if self._plugins: + raise RuntimeError("PluginManager already set!") + self._plugins = value @property def arch(self) -> CpuArch: @@ -175,18 +171,6 @@ class CoreSys: raise RuntimeError("Auth already set!") self._auth = value - @property - def audio(self) -> Audio: - """Return Audio object.""" - return self._audio - - @audio.setter - def audio(self, value: Audio): - """Set a Audio object.""" - if self._audio: - raise RuntimeError("Audio already set!") - self._audio = value - @property def homeassistant(self) -> HomeAssistant: """Return Home Assistant object.""" @@ -331,18 +315,6 @@ class CoreSys: raise RuntimeError("DBusManager already set!") self._dbus = value - @property - def dns(self) -> CoreDNS: - """Return CoreDNS object.""" - return self._dns - - @dns.setter - def dns(self, value: CoreDNS): - """Set a CoreDNS object.""" - if self._dns: - raise RuntimeError("CoreDNS already set!") - self._dns = value - @property def host(self) -> HostManager: """Return HostManager object.""" @@ -482,9 +454,9 @@ class CoreSysAttributes: return self.coresys.core @property - def sys_cli(self) -> HaCli: - """Return HaCli object.""" - return self.coresys.cli + def sys_plugins(self) -> PluginManager: + """Return PluginManager object.""" + return self.coresys.plugins @property def sys_arch(self) -> CpuArch: @@ -496,11 +468,6 @@ class CoreSysAttributes: """Return Auth object.""" return self.coresys.auth - @property - def sys_audio(self) -> Audio: - """Return Audio object.""" - return self.coresys.audio - @property def sys_homeassistant(self) -> HomeAssistant: """Return Home Assistant object.""" @@ -561,11 +528,6 @@ class CoreSysAttributes: """Return DBusManager object.""" return self.coresys.dbus - @property - def sys_dns(self) -> CoreDNS: - """Return CoreDNS object.""" - return self.coresys.dns - @property def sys_host(self) -> HostManager: """Return HostManager object.""" diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index 05dd420a3..f52b26332 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -308,11 +308,11 @@ class DockerAddon(DockerInterface): "bind": "/etc/pulse/client.conf", "mode": "ro", }, - str(self.sys_audio.path_extern_pulse): { + str(self.sys_plugins.audio.path_extern_pulse): { "bind": "/run/audio", "mode": "ro", }, - str(self.sys_audio.path_extern_asound): { + str(self.sys_plugins.audio.path_extern_asound): { "bind": "/etc/asound.conf", "mode": "ro", }, @@ -364,7 +364,7 @@ class DockerAddon(DockerInterface): _LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version) # Write data to DNS server - self.sys_dns.add_host(ipv4=self.ip_address, names=[self.addon.hostname]) + self.sys_plugins.dns.add_host(ipv4=self.ip_address, names=[self.addon.hostname]) def _install( self, tag: str, image: Optional[str] = None, latest: bool = False @@ -490,5 +490,5 @@ class DockerAddon(DockerInterface): Need run inside executor. """ if self.ip_address != NO_ADDDRESS: - self.sys_dns.delete_host(self.addon.hostname) + self.sys_plugins.dns.delete_host(self.addon.hostname) super()._stop(remove_container) diff --git a/supervisor/docker/audio.py b/supervisor/docker/audio.py index efa93099f..01c037c5a 100644 --- a/supervisor/docker/audio.py +++ b/supervisor/docker/audio.py @@ -20,7 +20,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes): @property def image(self) -> str: """Return name of Supervisor Audio image.""" - return self.sys_audio.image + return self.sys_plugins.audio.image @property def name(self) -> str: @@ -58,7 +58,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes): # Create & Run container docker_container = self.sys_docker.run( self.image, - version=self.sys_audio.version, + version=self.sys_plugins.audio.version, init=False, ipv4=self.sys_docker.network.audio, name=self.name, diff --git a/supervisor/docker/cli.py b/supervisor/docker/cli.py index 0f9fe6b27..a7b2ba3a5 100644 --- a/supervisor/docker/cli.py +++ b/supervisor/docker/cli.py @@ -18,7 +18,7 @@ class DockerCli(DockerInterface, CoreSysAttributes): @property def image(self): """Return name of HA cli image.""" - return self.sys_cli.image + return self.sys_plugins.cli.image @property def name(self) -> str: @@ -42,7 +42,7 @@ class DockerCli(DockerInterface, CoreSysAttributes): self.image, entrypoint=["/init"], command=["/bin/bash", "-c", "sleep infinity"], - version=self.sys_cli.version, + version=self.sys_plugins.cli.version, init=False, ipv4=self.sys_docker.network.cli, name=self.name, @@ -51,7 +51,7 @@ class DockerCli(DockerInterface, CoreSysAttributes): extra_hosts={"supervisor": self.sys_docker.network.supervisor}, environment={ ENV_TIME: self.sys_timezone, - ENV_TOKEN: self.sys_cli.supervisor_token, + ENV_TOKEN: self.sys_plugins.cli.supervisor_token, }, ) diff --git a/supervisor/docker/dns.py b/supervisor/docker/dns.py index a20068035..94f87eb1b 100644 --- a/supervisor/docker/dns.py +++ b/supervisor/docker/dns.py @@ -18,7 +18,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes): @property def image(self) -> str: """Return name of Supervisor DNS image.""" - return self.sys_dns.image + return self.sys_plugins.dns.image @property def name(self) -> str: @@ -40,7 +40,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes): # Create & Run container docker_container = self.sys_docker.run( self.image, - version=self.sys_dns.version, + version=self.sys_plugins.dns.version, init=False, dns=False, ipv4=self.sys_docker.network.dns, diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 95cc3e419..dd94b5963 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -72,11 +72,11 @@ class DockerHomeAssistant(DockerInterface): "bind": "/etc/pulse/client.conf", "mode": "ro", }, - str(self.sys_audio.path_extern_pulse): { + str(self.sys_plugins.audio.path_extern_pulse): { "bind": "/run/audio", "mode": "ro", }, - str(self.sys_audio.path_extern_asound): { + str(self.sys_plugins.audio.path_extern_asound): { "bind": "/etc/asound.conf", "mode": "ro", }, diff --git a/supervisor/docker/multicast.py b/supervisor/docker/multicast.py new file mode 100644 index 000000000..7a033d4ea --- /dev/null +++ b/supervisor/docker/multicast.py @@ -0,0 +1,59 @@ +"""HA Cli docker object.""" +from contextlib import suppress +import logging + +from ..const import DOCKER_NETWORK_MASK, ENV_HASSIO_NETWORK, ENV_TIME +from ..coresys import CoreSysAttributes +from ..exceptions import DockerAPIError +from .interface import DockerInterface + +_LOGGER: logging.Logger = logging.getLogger(__name__) + +MULTICAST_DOCKER_NAME: str = "hassio_multicast" + + +class DockerMulticast(DockerInterface, CoreSysAttributes): + """Docker Supervisor wrapper for HA multicast.""" + + @property + def image(self): + """Return name of HA multicast image.""" + return self.sys_plugins.multicast.image + + @property + def name(self) -> str: + """Return name of Docker container.""" + return MULTICAST_DOCKER_NAME + + def _run(self) -> None: + """Run Docker image. + + Need run inside executor. + """ + if self._is_running(): + return + + # Cleanup + with suppress(DockerAPIError): + self._stop() + + # Create & Run container + docker_container = self.sys_docker.run( + self.image, + version=self.sys_plugins.multicast.version, + init=False, + name=self.name, + hostname=self.name.replace("_", "-"), + network_mode="host", + detach=True, + extra_hosts={"supervisor": self.sys_docker.network.supervisor}, + environment={ + ENV_TIME: self.sys_timezone, + ENV_HASSIO_NETWORK: str(DOCKER_NETWORK_MASK), + }, + ) + + self._meta = docker_container.attrs + _LOGGER.info( + "Start Multicast %s with version %s - Host", self.image, self.version + ) diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index c5484daff..b6b89f004 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -61,10 +61,21 @@ class CliError(HassioError): """HA cli exception.""" -class CliUpdateError(HassOSError): +class CliUpdateError(CliError): """Error on update of a HA cli.""" +# Multicast + + +class MulticastError(HassioError): + """Multicast exception.""" + + +class MulticastUpdateError(MulticastError): + """Error on update of a multicast.""" + + # DNS diff --git a/supervisor/homeassistant.py b/supervisor/homeassistant.py index c9fc68f77..decc49bf3 100644 --- a/supervisor/homeassistant.py +++ b/supervisor/homeassistant.py @@ -636,7 +636,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def write_pulse(self): """Write asound config to file and return True on success.""" - pulse_config = self.sys_audio.pulse_client( + pulse_config = self.sys_plugins.audio.pulse_client( input_profile=self.audio_input, output_profile=self.audio_output ) diff --git a/supervisor/plugins/__init__.py b/supervisor/plugins/__init__.py index d2cffc4d9..c41c7cebf 100644 --- a/supervisor/plugins/__init__.py +++ b/supervisor/plugins/__init__.py @@ -1 +1,76 @@ """Plugin for Supervisor backend.""" +import asyncio +import logging + +from ..coresys import CoreSys, CoreSysAttributes +from .audio import Audio +from .cli import HaCli +from .dns import CoreDNS +from .multicast import Multicast + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class PluginManager(CoreSysAttributes): + """Manage supported function for plugins.""" + + def __init__(self, coresys: CoreSys): + """Initialize plugin manager.""" + self.coresys: CoreSys = coresys + + self._cli: HaCli = HaCli(coresys) + self._dns: CoreDNS = CoreDNS(coresys) + self._audio: Audio = Audio(coresys) + self._multicast: Multicast = Multicast(coresys) + + @property + def cli(self) -> HaCli: + """Return cli handler.""" + return self._cli + + @property + def dns(self) -> CoreDNS: + """Return dns handler.""" + return self._dns + + @property + def audio(self) -> Audio: + """Return audio handler.""" + return self._audio + + @property + def multicast(self) -> Multicast: + """Return multicast handler.""" + return self._multicast + + async def load(self): + """Load Supervisor plugins.""" + await asyncio.wait( + [self.dns.load(), self.audio.load(), self.cli.load(), self.multicast.load()] + ) + + async def repair(self): + """Repair Supervisor plugins.""" + await asyncio.wait( + [ + self.dns.repair(), + self.audio.repair(), + self.cli.repair(), + self.multicast.repair(), + ] + ) + + async def unload(self) -> None: + """Unload Supervisor plugin.""" + await asyncio.wait([self.dns.unload()]) + + async def shutdown(self) -> None: + """Shutdown Supervisor plugin.""" + await asyncio.wait( + [ + self.dns.stop(), + self.audio.stop(), + self.cli.stop(), + self.multicast.stop(), + ] + ) diff --git a/supervisor/plugins/audio.py b/supervisor/plugins/audio.py index 1cd68a099..05a42ad7b 100644 --- a/supervisor/plugins/audio.py +++ b/supervisor/plugins/audio.py @@ -1,4 +1,7 @@ -"""Home Assistant control object.""" +"""Home Assistant audio plugin. + +Code: https://github.com/home-assistant/plugin-audio +""" import asyncio from contextlib import suppress import logging @@ -14,12 +17,12 @@ from ..docker.audio import DockerAudio from ..docker.stats import DockerStats from ..exceptions import AudioError, AudioUpdateError, DockerAPIError from ..utils.json import JsonConfig -from ..validate import SCHEMA_AUDIO_CONFIG +from .validate import SCHEMA_AUDIO_CONFIG _LOGGER: logging.Logger = logging.getLogger(__name__) -PULSE_CLIENT_TMPL: Path = Path(__file__).parents[0].joinpath("data/pulse-client.tmpl") -ASOUND_TMPL: Path = Path(__file__).parents[0].joinpath("data/asound.tmpl") +PULSE_CLIENT_TMPL: Path = Path(__file__).parents[1].joinpath("data/pulse-client.tmpl") +ASOUND_TMPL: Path = Path(__file__).parents[1].joinpath("data/asound.tmpl") class Audio(JsonConfig, CoreSysAttributes): @@ -177,7 +180,6 @@ class Audio(JsonConfig, CoreSysAttributes): async def start(self) -> None: """Run CoreDNS.""" - # Start Instance _LOGGER.info("Start Audio plugin") try: await self.instance.run() @@ -185,6 +187,15 @@ class Audio(JsonConfig, CoreSysAttributes): _LOGGER.error("Can't start Audio plugin") raise AudioError() from None + async def stop(self) -> None: + """Stop CoreDNS.""" + _LOGGER.info("Stop Audio plugin") + try: + await self.instance.stop() + except DockerAPIError: + _LOGGER.error("Can't stop Audio plugin") + raise AudioError() from None + def logs(self) -> Awaitable[bytes]: """Get CoreDNS docker logs. diff --git a/supervisor/plugins/cli.py b/supervisor/plugins/cli.py index 4d7e1fdc7..a26e0ee94 100644 --- a/supervisor/plugins/cli.py +++ b/supervisor/plugins/cli.py @@ -1,4 +1,7 @@ -"""CLI support on supervisor.""" +"""Home Assistant cli plugin. + +Code: https://github.com/home-assistant/plugin-cli +""" import asyncio from contextlib import suppress import logging @@ -11,7 +14,7 @@ from ..docker.cli import DockerCli from ..docker.stats import DockerStats from ..exceptions import CliError, CliUpdateError, DockerAPIError from ..utils.json import JsonConfig -from ..validate import SCHEMA_CLI_CONFIG +from .validate import SCHEMA_CLI_CONFIG _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -158,6 +161,15 @@ class HaCli(CoreSysAttributes, JsonConfig): _LOGGER.error("Can't start cli plugin") raise CliError() from None + async def stop(self) -> None: + """Stop cli.""" + _LOGGER.info("Stop cli plugin") + try: + await self.instance.stop() + except DockerAPIError: + _LOGGER.error("Can't stop cli plugin") + raise CliError() from None + async def stats(self) -> DockerStats: """Return stats of cli.""" try: diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index c6e9ec30b..f7c8abd63 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -1,4 +1,7 @@ -"""Home Assistant control object.""" +"""Home Assistant dns plugin. + +Code: https://github.com/home-assistant/plugin-dns +""" import asyncio from contextlib import suppress from ipaddress import IPv4Address @@ -17,12 +20,13 @@ from ..docker.stats import DockerStats from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError from ..misc.forwarder import DNSForward from ..utils.json import JsonConfig -from ..validate import SCHEMA_DNS_CONFIG, dns_url +from .validate import SCHEMA_DNS_CONFIG +from ..validate import dns_url _LOGGER: logging.Logger = logging.getLogger(__name__) -COREDNS_TMPL: Path = Path(__file__).parents[0].joinpath("data/coredns.tmpl") -RESOLV_TMPL: Path = Path(__file__).parents[0].joinpath("data/resolv.tmpl") +COREDNS_TMPL: Path = Path(__file__).parents[1].joinpath("data/coredns.tmpl") +RESOLV_TMPL: Path = Path(__file__).parents[1].joinpath("data/resolv.tmpl") HOST_RESOLV: Path = Path("/etc/resolv.conf") @@ -212,8 +216,12 @@ class CoreDNS(JsonConfig, CoreSysAttributes): async def restart(self) -> None: """Restart CoreDNS plugin.""" self._write_corefile() - with suppress(DockerAPIError): + _LOGGER.info("Restart CoreDNS plugin") + try: await self.instance.restart() + except DockerAPIError: + _LOGGER.error("Can't start CoreDNS plugin") + raise CoreDNSError() async def start(self) -> None: """Run CoreDNS.""" @@ -227,6 +235,15 @@ class CoreDNS(JsonConfig, CoreSysAttributes): _LOGGER.error("Can't start CoreDNS plugin") raise CoreDNSError() from None + async def stop(self) -> None: + """Stop CoreDNS.""" + _LOGGER.info("Stop CoreDNS plugin") + try: + await self.instance.stop() + except DockerAPIError: + _LOGGER.error("Can't stop CoreDNS plugin") + raise CoreDNSError() from None + async def reset(self) -> None: """Reset DNS and hosts.""" # Reset manually defined DNS diff --git a/supervisor/plugins/multicast.py b/supervisor/plugins/multicast.py new file mode 100644 index 000000000..088b97f3f --- /dev/null +++ b/supervisor/plugins/multicast.py @@ -0,0 +1,208 @@ +"""Home Assistant multicast plugin. + +Code: https://github.com/home-assistant/plugin-multicast +""" +import asyncio +from contextlib import suppress +import logging +from typing import Awaitable, Optional + +from ..const import ATTR_IMAGE, ATTR_VERSION, FILE_HASSIO_MULTICAST +from ..coresys import CoreSys, CoreSysAttributes +from ..docker.multicast import DockerMulticast +from ..docker.stats import DockerStats +from ..exceptions import DockerAPIError, MulticastError, MulticastUpdateError +from ..utils.json import JsonConfig +from .validate import SCHEMA_MULTICAST_CONFIG + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class Multicast(JsonConfig, CoreSysAttributes): + """Home Assistant core object for handle it.""" + + def __init__(self, coresys: CoreSys): + """Initialize hass object.""" + super().__init__(FILE_HASSIO_MULTICAST, SCHEMA_MULTICAST_CONFIG) + self.coresys: CoreSys = coresys + self.instance: DockerMulticast = DockerMulticast(coresys) + + @property + def version(self) -> Optional[str]: + """Return current version of Multicast.""" + return self._data.get(ATTR_VERSION) + + @version.setter + def version(self, value: str) -> None: + """Return current version of Multicast.""" + self._data[ATTR_VERSION] = value + + @property + def image(self) -> str: + """Return current image of Multicast.""" + if self._data.get(ATTR_IMAGE): + return self._data[ATTR_IMAGE] + return f"homeassistant/{self.sys_arch.supervisor}-hassio-multicast" + + @image.setter + def image(self, value: str) -> None: + """Return current image of Multicast.""" + self._data[ATTR_IMAGE] = value + + @property + def latest_version(self) -> Optional[str]: + """Return latest version of Multicast.""" + return self.sys_updater.version_multicast + + @property + def in_progress(self) -> bool: + """Return True if a task is in progress.""" + return self.instance.in_progress + + @property + def need_update(self) -> bool: + """Return True if an update is available.""" + return self.version != self.latest_version + + async def load(self) -> None: + """Load multicast setup.""" + # Check Multicast state + try: + # Evaluate Version if we lost this information + if not self.version: + self.version = await self.instance.get_latest_version(key=int) + + await self.instance.attach(tag=self.version) + except DockerAPIError: + _LOGGER.info( + "No Multicast plugin Docker image %s found.", self.instance.image + ) + + # Install Multicast plugin + with suppress(MulticastError): + await self.install() + else: + self.version = self.instance.version + self.image = self.instance.image + self.save_data() + + # Run Multicast plugin + with suppress(MulticastError): + if await self.instance.is_running(): + await self.restart() + else: + await self.start() + + async def install(self) -> None: + """Install Multicast.""" + _LOGGER.info("Setup Multicast plugin") + while True: + # read homeassistant tag and install it + if not self.latest_version: + await self.sys_updater.reload() + + if self.latest_version: + with suppress(DockerAPIError): + await self.instance.install( + self.latest_version, image=self.sys_updater.image_multicast + ) + break + _LOGGER.warning("Error on install Multicast plugin. Retry in 30sec") + await asyncio.sleep(30) + + _LOGGER.info("Multicast plugin now installed") + self.version = self.instance.version + self.image = self.sys_updater.image_multicast + self.save_data() + + async def update(self, version: Optional[str] = None) -> None: + """Update Multicast plugin.""" + version = version or self.latest_version + old_image = self.image + + if version == self.version: + _LOGGER.warning("Version %s is already installed for Multicast", version) + return + + # Update + try: + await self.instance.update(version, image=self.sys_updater.image_multicast) + except DockerAPIError: + _LOGGER.error("Multicast update fails") + raise MulticastUpdateError() from None + else: + self.version = version + self.image = self.sys_updater.image_multicast + self.save_data() + + # Cleanup + with suppress(DockerAPIError): + await self.instance.cleanup(old_image=old_image) + + # Start Multicast plugin + await self.start() + + async def restart(self) -> None: + """Restart Multicast plugin.""" + _LOGGER.info("Restart Multicast plugin") + try: + await self.instance.restart() + except DockerAPIError: + _LOGGER.error("Can't start Multicast plugin") + raise MulticastError() + + async def start(self) -> None: + """Run Multicast.""" + _LOGGER.info("Start Multicast plugin") + try: + await self.instance.run() + except DockerAPIError: + _LOGGER.error("Can't start Multicast plugin") + raise MulticastError() + + async def stop(self) -> None: + """Stop Multicast.""" + _LOGGER.info("Stop Multicast plugin") + try: + await self.instance.stop() + except DockerAPIError: + _LOGGER.error("Can't stop Multicast plugin") + raise MulticastError() + + def logs(self) -> Awaitable[bytes]: + """Get Multicast docker logs. + + Return Coroutine. + """ + return self.instance.logs() + + async def stats(self) -> DockerStats: + """Return stats of Multicast.""" + try: + return await self.instance.stats() + except DockerAPIError: + raise MulticastError() from None + + def is_running(self) -> Awaitable[bool]: + """Return True if Docker container is running. + + Return a coroutine. + """ + return self.instance.is_running() + + def is_fails(self) -> Awaitable[bool]: + """Return True if a Docker container is fails state. + Return a coroutine. + """ + return self.instance.is_fails() + + async def repair(self) -> None: + """Repair Multicast plugin.""" + if await self.instance.exists(): + return + + _LOGGER.info("Repair Multicast %s", self.version) + try: + await self.instance.install(self.version) + except DockerAPIError: + _LOGGER.error("Repairing of Multicast fails") diff --git a/supervisor/plugins/validate.py b/supervisor/plugins/validate.py new file mode 100644 index 000000000..346884afa --- /dev/null +++ b/supervisor/plugins/validate.py @@ -0,0 +1,44 @@ +"""Validate functions.""" + +import voluptuous as vol + +from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION +from ..validate import dns_server_list, docker_image, token + + +SCHEMA_DNS_CONFIG = vol.Schema( + { + vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_IMAGE): docker_image, + vol.Optional(ATTR_SERVERS, default=list): dns_server_list, + }, + extra=vol.REMOVE_EXTRA, +) + + +SCHEMA_AUDIO_CONFIG = vol.Schema( + { + vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_IMAGE): docker_image, + }, + extra=vol.REMOVE_EXTRA, +) + + +SCHEMA_CLI_CONFIG = vol.Schema( + { + vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_IMAGE): docker_image, + vol.Optional(ATTR_ACCESS_TOKEN): token, + }, + extra=vol.REMOVE_EXTRA, +) + + +SCHEMA_MULTICAST_CONFIG = vol.Schema( + { + vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), + vol.Optional(ATTR_IMAGE): docker_image, + }, + extra=vol.REMOVE_EXTRA, +) diff --git a/supervisor/tasks.py b/supervisor/tasks.py index 4aa569e40..c146c8c39 100644 --- a/supervisor/tasks.py +++ b/supervisor/tasks.py @@ -3,7 +3,13 @@ import asyncio import logging from .coresys import CoreSysAttributes -from .exceptions import AudioError, CliError, CoreDNSError, HomeAssistantError +from .exceptions import ( + AudioError, + CliError, + CoreDNSError, + HomeAssistantError, + MulticastError, +) _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -14,6 +20,7 @@ RUN_UPDATE_ADDONS = 57600 RUN_UPDATE_CLI = 28100 RUN_UPDATE_DNS = 30100 RUN_UPDATE_AUDIO = 30200 +RUN_UPDATE_MULTICAST = 30300 RUN_RELOAD_ADDONS = 10800 RUN_RELOAD_SNAPSHOTS = 72000 @@ -27,6 +34,7 @@ RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_DNS_DOCKER = 20 RUN_WATCHDOG_AUDIO_DOCKER = 30 RUN_WATCHDOG_CLI_DOCKER = 40 +RUN_WATCHDOG_MULTICAST_DOCKER = 50 class Tasks(CoreSysAttributes): @@ -58,6 +66,11 @@ class Tasks(CoreSysAttributes): self.jobs.add( self.sys_scheduler.register_task(self._update_audio, RUN_UPDATE_AUDIO) ) + self.jobs.add( + self.sys_scheduler.register_task( + self._update_multicast, RUN_UPDATE_MULTICAST + ) + ) # Reload self.jobs.add( @@ -108,6 +121,11 @@ class Tasks(CoreSysAttributes): self._watchdog_cli_docker, RUN_WATCHDOG_CLI_DOCKER ) ) + self.jobs.add( + self.sys_scheduler.register_task( + self._watchdog_multicast_docker, RUN_WATCHDOG_MULTICAST_DOCKER + ) + ) _LOGGER.info("All core tasks are scheduled") @@ -209,66 +227,92 @@ class Tasks(CoreSysAttributes): async def _update_cli(self): """Check and run update of cli.""" - if not self.sys_cli.need_update: + if not self.sys_plugins.cli.need_update: return _LOGGER.info("Found new cli version") - await self.sys_cli.update() + await self.sys_plugins.cli.update() async def _update_dns(self): """Check and run update of CoreDNS plugin.""" - if not self.sys_dns.need_update: + if not self.sys_plugins.dns.need_update: return _LOGGER.info("Found new CoreDNS plugin version") - await self.sys_dns.update() + await self.sys_plugins.dns.update() async def _update_audio(self): """Check and run update of PulseAudio plugin.""" - if not self.sys_audio.need_update: + if not self.sys_plugins.audio.need_update: return _LOGGER.info("Found new PulseAudio plugin version") - await self.sys_audio.update() + await self.sys_plugins.audio.update() + + async def _update_multicast(self): + """Check and run update of multicast.""" + if not self.sys_plugins.multicast.need_update: + return + + _LOGGER.info("Found new Multicast version") + await self.sys_plugins.multicast.update() async def _watchdog_dns_docker(self): """Check running state of Docker and start if they is close.""" # if CoreDNS is active - if await self.sys_dns.is_running() or self.sys_dns.in_progress: + if await self.sys_plugins.dns.is_running() or self.sys_plugins.dns.in_progress: return _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") # Reset of fails - if await self.sys_dns.is_fails(): + if await self.sys_plugins.dns.is_fails(): _LOGGER.error("CoreDNS plugin is in fails state / Reset config") - await self.sys_dns.reset() - await self.sys_dns.loop_detection() + await self.sys_plugins.dns.reset() + await self.sys_plugins.dns.loop_detection() try: - await self.sys_dns.start() + await self.sys_plugins.dns.start() except CoreDNSError: _LOGGER.error("Watchdog CoreDNS reanimation fails!") async def _watchdog_audio_docker(self): """Check running state of Docker and start if they is close.""" # if PulseAudio plugin is active - if await self.sys_audio.is_running() or self.sys_audio.in_progress: + if ( + await self.sys_plugins.audio.is_running() + or self.sys_plugins.audio.in_progress + ): return _LOGGER.warning("Watchdog found a problem with PulseAudio plugin!") try: - await self.sys_audio.start() + await self.sys_plugins.audio.start() except AudioError: _LOGGER.error("Watchdog PulseAudio reanimation fails!") async def _watchdog_cli_docker(self): """Check running state of Docker and start if they is close.""" # if cli plugin is active - if await self.sys_cli.is_running() or self.sys_cli.in_progress: + if await self.sys_plugins.cli.is_running() or self.sys_plugins.cli.in_progress: return _LOGGER.warning("Watchdog found a problem with cli plugin!") try: - await self.sys_cli.start() + await self.sys_plugins.cli.start() except CliError: _LOGGER.error("Watchdog cli reanimation fails!") + + async def _watchdog_multicast_docker(self): + """Check running state of Docker and start if they is close.""" + # if multicast plugin is active + if ( + await self.sys_plugins.multicast.is_running() + or self.sys_plugins.multicast.in_progress + ): + return + _LOGGER.warning("Watchdog found a problem with Multicast plugin!") + + try: + await self.sys_plugins.multicast.start() + except MulticastError: + _LOGGER.error("Watchdog Multicast reanimation fails!") diff --git a/supervisor/updater.py b/supervisor/updater.py index cffe86e84..81f8aa109 100644 --- a/supervisor/updater.py +++ b/supervisor/updater.py @@ -13,10 +13,11 @@ from .const import ( ATTR_CHANNEL, ATTR_CLI, ATTR_DNS, - ATTR_SUPERVISOR, ATTR_HASSOS, ATTR_HOMEASSISTANT, ATTR_IMAGE, + ATTR_MULTICAST, + ATTR_SUPERVISOR, FILE_HASSIO_UPDATER, URL_HASSIO_VERSION, UpdateChannels, @@ -78,6 +79,11 @@ class Updater(JsonConfig, CoreSysAttributes): """Return latest version of Audio.""" return self._data.get(ATTR_AUDIO) + @property + def version_multicast(self) -> Optional[str]: + """Return latest version of Multicast.""" + return self._data.get(ATTR_MULTICAST) + @property def image_homeassistant(self) -> Optional[str]: """Return latest version of Home Assistant.""" @@ -123,6 +129,15 @@ class Updater(JsonConfig, CoreSysAttributes): .format(arch=self.sys_arch.supervisor) ) + @property + def image_multicast(self) -> Optional[str]: + """Return latest version of Multicast.""" + return ( + self._data[ATTR_IMAGE] + .get(ATTR_MULTICAST, "") + .format(arch=self.sys_arch.supervisor) + ) + @property def channel(self) -> UpdateChannels: """Return upstream channel of Supervisor instance.""" @@ -171,10 +186,11 @@ class Updater(JsonConfig, CoreSysAttributes): if self.sys_hassos.board: self._data[ATTR_HASSOS] = data["hassos"][self.sys_hassos.board] - # Update Home Assistant services + # Update Home Assistant plugins self._data[ATTR_CLI] = data["cli"] self._data[ATTR_DNS] = data["dns"] self._data[ATTR_AUDIO] = data["audio"] + self._data[ATTR_MULTICAST] = data["multicast"] # Update images for that versions self._data[ATTR_IMAGE][ATTR_HOMEASSISTANT] = data["image"]["core"] @@ -182,6 +198,7 @@ class Updater(JsonConfig, CoreSysAttributes): self._data[ATTR_IMAGE][ATTR_AUDIO] = data["image"]["audio"] self._data[ATTR_IMAGE][ATTR_CLI] = data["image"]["cli"] self._data[ATTR_IMAGE][ATTR_DNS] = data["image"]["dns"] + self._data[ATTR_IMAGE][ATTR_MULTICAST] = data["image"]["multicast"] except KeyError as err: _LOGGER.warning("Can't process version data: %s", err) diff --git a/supervisor/validate.py b/supervisor/validate.py index ceb45a6ec..4e502073b 100644 --- a/supervisor/validate.py +++ b/supervisor/validate.py @@ -17,18 +17,18 @@ from .const import ( ATTR_DEBUG, ATTR_DEBUG_BLOCK, ATTR_DNS, - ATTR_SUPERVISOR, ATTR_HASSOS, ATTR_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_BOOT, ATTR_LOGGING, + ATTR_MULTICAST, ATTR_PORT, ATTR_PORTS, ATTR_REFRESH_TOKEN, - ATTR_SERVERS, ATTR_SESSION, ATTR_SSL, + ATTR_SUPERVISOR, ATTR_TIMEZONE, ATTR_UUID, ATTR_VERSION, @@ -129,6 +129,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema( vol.Optional(ATTR_CLI): vol.Coerce(str), vol.Optional(ATTR_DNS): vol.Coerce(str), vol.Optional(ATTR_AUDIO): vol.Coerce(str), + vol.Optional(ATTR_MULTICAST): vol.Coerce(str), vol.Optional(ATTR_IMAGE, default=dict): vol.Schema( { vol.Optional(ATTR_HOMEASSISTANT): docker_image, @@ -136,6 +137,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema( vol.Optional(ATTR_CLI): docker_image, vol.Optional(ATTR_DNS): docker_image, vol.Optional(ATTR_AUDIO): docker_image, + vol.Optional(ATTR_MULTICAST): docker_image, }, extra=vol.REMOVE_EXTRA, ), @@ -176,32 +178,3 @@ SCHEMA_INGRESS_CONFIG = vol.Schema( }, extra=vol.REMOVE_EXTRA, ) - - -SCHEMA_DNS_CONFIG = vol.Schema( - { - vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), - vol.Optional(ATTR_IMAGE): docker_image, - vol.Optional(ATTR_SERVERS, default=list): dns_server_list, - }, - extra=vol.REMOVE_EXTRA, -) - - -SCHEMA_AUDIO_CONFIG = vol.Schema( - { - vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), - vol.Optional(ATTR_IMAGE): docker_image, - }, - extra=vol.REMOVE_EXTRA, -) - - -SCHEMA_CLI_CONFIG = vol.Schema( - { - vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)), - vol.Optional(ATTR_IMAGE): docker_image, - vol.Optional(ATTR_ACCESS_TOKEN): token, - }, - extra=vol.REMOVE_EXTRA, -)