mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-28 03:26:32 +00:00
commit
3b2351af0b
37
API.md
37
API.md
@ -851,6 +851,8 @@ return:
|
||||
"hassos": "null|version",
|
||||
"docker": "version",
|
||||
"hostname": "name",
|
||||
"operating_system": "HassOS XY|Ubuntu 16.4|null",
|
||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||
"machine": "type",
|
||||
"arch": "arch",
|
||||
"supported_arch": ["arch1", "arch2"],
|
||||
@ -946,6 +948,41 @@ return:
|
||||
}
|
||||
```
|
||||
|
||||
### Observer
|
||||
|
||||
- GET `/observer/info`
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "ip-address",
|
||||
"version": "1",
|
||||
"version_latest": "2"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/observer/update`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "VERSION"
|
||||
}
|
||||
```
|
||||
|
||||
- GET `/observer/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
|
||||
}
|
||||
```
|
||||
|
||||
### Multicast
|
||||
|
||||
- GET `/multicast/info`
|
||||
|
@ -14,6 +14,6 @@ pulsectl==20.5.1
|
||||
pytz==2020.1
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.17.3
|
||||
sentry-sdk==0.17.4
|
||||
uvloop==0.14.0
|
||||
voluptuous==0.11.7
|
||||
|
@ -105,7 +105,7 @@ from ..validate import (
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|ro))?$")
|
||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share|media)(?::(rw|ro))?$")
|
||||
RE_SERVICE = re.compile(r"^(?P<service>mqtt|mysql):(?P<rights>provide|want|need)$")
|
||||
|
||||
V_STR = "str"
|
||||
|
@ -19,6 +19,7 @@ from .info import APIInfo
|
||||
from .ingress import APIIngress
|
||||
from .multicast import APIMulticast
|
||||
from .network import APINetwork
|
||||
from .observer import APIObserver
|
||||
from .os import APIOS
|
||||
from .proxy import APIProxy
|
||||
from .security import SecurityMiddleware
|
||||
@ -54,6 +55,7 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_host()
|
||||
self._register_os()
|
||||
self._register_cli()
|
||||
self._register_observer()
|
||||
self._register_multicast()
|
||||
self._register_network()
|
||||
self._register_hardware()
|
||||
@ -135,6 +137,19 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_observer(self) -> None:
|
||||
"""Register Observer functions."""
|
||||
api_observer = APIObserver()
|
||||
api_observer.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/observer/info", api_observer.info),
|
||||
web.get("/observer/stats", api_observer.stats),
|
||||
web.post("/observer/update", api_observer.update),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_multicast(self) -> None:
|
||||
"""Register Multicast functions."""
|
||||
api_multicast = APIMulticast()
|
||||
|
@ -8,11 +8,13 @@ from ..const import (
|
||||
ATTR_ARCH,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_DOCKER,
|
||||
ATTR_FEATURES,
|
||||
ATTR_HASSOS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_HOSTNAME,
|
||||
ATTR_LOGGING,
|
||||
ATTR_MACHINE,
|
||||
ATTR_OPERATING_SYSTEM,
|
||||
ATTR_SUPERVISOR,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_SUPPORTED_ARCH,
|
||||
@ -36,6 +38,8 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_HASSOS: self.sys_hassos.version,
|
||||
ATTR_DOCKER: self.sys_docker.info.version,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
ATTR_FEATURES: self.sys_host.supported_features,
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
|
65
supervisor/api/observer.py
Normal file
65
supervisor/api/observer.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Init file for Supervisor Observer RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import (
|
||||
ATTR_BLK_READ,
|
||||
ATTR_BLK_WRITE,
|
||||
ATTR_CPU_PERCENT,
|
||||
ATTR_HOST,
|
||||
ATTR_MEMORY_LIMIT,
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_MEMORY_USAGE,
|
||||
ATTR_NETWORK_RX,
|
||||
ATTR_NETWORK_TX,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..validate import version_tag
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||
|
||||
|
||||
class APIObserver(CoreSysAttributes):
|
||||
"""Handle RESTful API for Observer functions."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return HA Observer information."""
|
||||
return {
|
||||
ATTR_HOST: str(self.sys_docker.network.observer),
|
||||
ATTR_VERSION: self.sys_plugins.observer.version,
|
||||
ATTR_VERSION_LATEST: self.sys_plugins.observer.latest_version,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return resource information."""
|
||||
stats = await self.sys_plugins.observer.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 HA observer."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.sys_plugins.observer.latest_version)
|
||||
|
||||
await asyncio.shield(self.sys_plugins.observer.update(version))
|
@ -39,17 +39,22 @@ NO_SECURITY_CHECK = re.compile(
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Observer allow API calls
|
||||
OBSERVER_CHECK = re.compile(
|
||||
r"^(?:"
|
||||
r"|/[^/]+/info"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Can called by every add-on
|
||||
ADDONS_API_BYPASS = re.compile(
|
||||
r"^(?:"
|
||||
r"|/addons/self/(?!security|update)[^/]+"
|
||||
r"|/secrets/.+"
|
||||
r"|/info"
|
||||
r"|/hardware/trigger"
|
||||
r"|/services.*"
|
||||
r"|/discovery.*"
|
||||
r"|/auth"
|
||||
r"|/host/info"
|
||||
r")$"
|
||||
)
|
||||
|
||||
@ -95,7 +100,7 @@ ADDONS_ROLE_ACCESS = {
|
||||
),
|
||||
}
|
||||
|
||||
# fmt: off
|
||||
# fmt: on
|
||||
|
||||
|
||||
class SecurityMiddleware(CoreSysAttributes):
|
||||
@ -136,6 +141,14 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
_LOGGER.debug("%s access from Host", request.path)
|
||||
request_from = self.sys_host
|
||||
|
||||
# Observer
|
||||
if supervisor_token == self.sys_plugins.observer.supervisor_token:
|
||||
if not OBSERVER_CHECK.match(request.url):
|
||||
_LOGGER.warning("%s invalid Observer access", request.path)
|
||||
raise HTTPForbidden()
|
||||
_LOGGER.debug("%s access from Observer", request.path)
|
||||
request_from = self.sys_plugins.observer
|
||||
|
||||
# Add-on
|
||||
addon = None
|
||||
if supervisor_token and not request_from:
|
||||
|
@ -164,6 +164,11 @@ def initialize_system_data(coresys: CoreSys) -> None:
|
||||
_LOGGER.info("Create Supervisor audio folder %s", config.path_audio)
|
||||
config.path_audio.mkdir()
|
||||
|
||||
# Media folder
|
||||
if not config.path_media.is_dir():
|
||||
_LOGGER.info("Create Supervisor media folder %s", config.path_media)
|
||||
config.path_media.mkdir()
|
||||
|
||||
# Update log level
|
||||
coresys.config.modify_log_level()
|
||||
|
||||
|
@ -41,6 +41,7 @@ TMP_DATA = PurePath("tmp")
|
||||
APPARMOR_DATA = PurePath("apparmor")
|
||||
DNS_DATA = PurePath("dns")
|
||||
AUDIO_DATA = PurePath("audio")
|
||||
MEDIA_DATA = PurePath("media")
|
||||
|
||||
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
|
||||
|
||||
@ -258,6 +259,16 @@ class CoreConfig(JsonConfig):
|
||||
"""Return dns path inside supervisor."""
|
||||
return Path(SUPERVISOR_DATA, DNS_DATA)
|
||||
|
||||
@property
|
||||
def path_media(self) -> Path:
|
||||
"""Return root media data folder."""
|
||||
return Path(SUPERVISOR_DATA, MEDIA_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_media(self) -> PurePath:
|
||||
"""Return root media data folder external for Docker."""
|
||||
return PurePath(self.path_extern_supervisor, MEDIA_DATA)
|
||||
|
||||
@property
|
||||
def addons_repositories(self) -> List[str]:
|
||||
"""Return list of custom Add-on repositories."""
|
||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
SUPERVISOR_VERSION = "242"
|
||||
SUPERVISOR_VERSION = "243"
|
||||
|
||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||
URL_HASSIO_APPARMOR = "https://version.home-assistant.io/apparmor.txt"
|
||||
@ -17,15 +17,11 @@ URL_HASSOS_OTA = (
|
||||
SUPERVISOR_DATA = Path("/data")
|
||||
|
||||
FILE_HASSIO_ADDONS = Path(SUPERVISOR_DATA, "addons.json")
|
||||
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
||||
FILE_HASSIO_AUTH = Path(SUPERVISOR_DATA, "auth.json")
|
||||
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
||||
FILE_HASSIO_CONFIG = Path(SUPERVISOR_DATA, "config.json")
|
||||
FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json")
|
||||
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
||||
FILE_HASSIO_HOMEASSISTANT = Path(SUPERVISOR_DATA, "homeassistant.json")
|
||||
FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json")
|
||||
FILE_HASSIO_MULTICAST = Path(SUPERVISOR_DATA, "multicast.json")
|
||||
FILE_HASSIO_SERVICES = Path(SUPERVISOR_DATA, "services.json")
|
||||
FILE_HASSIO_UPDATER = Path(SUPERVISOR_DATA, "updater.json")
|
||||
|
||||
@ -74,7 +70,7 @@ HEADER_TOKEN_OLD = "X-Hassio-Key"
|
||||
|
||||
ENV_TIME = "TZ"
|
||||
ENV_TOKEN = "SUPERVISOR_TOKEN"
|
||||
ENV_TOKEN_OLD = "HASSIO_TOKEN"
|
||||
ENV_TOKEN_HASSIO = "HASSIO_TOKEN"
|
||||
|
||||
ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY"
|
||||
ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV"
|
||||
@ -275,6 +271,7 @@ ATTR_VPN = "vpn"
|
||||
ATTR_WAIT_BOOT = "wait_boot"
|
||||
ATTR_WATCHDOG = "watchdog"
|
||||
ATTR_WEBUI = "webui"
|
||||
ATTR_OBSERVER = "observer"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
@ -289,6 +286,7 @@ MAP_SSL = "ssl"
|
||||
MAP_ADDONS = "addons"
|
||||
MAP_BACKUP = "backup"
|
||||
MAP_SHARE = "share"
|
||||
MAP_MEDIA = "media"
|
||||
|
||||
ARCH_ARMHF = "armhf"
|
||||
ARCH_ARMV7 = "armv7"
|
||||
@ -305,6 +303,7 @@ FOLDER_HOMEASSISTANT = "homeassistant"
|
||||
FOLDER_SHARE = "share"
|
||||
FOLDER_ADDONS = "addons/local"
|
||||
FOLDER_SSL = "ssl"
|
||||
FOLDER_MEDIA = "media"
|
||||
|
||||
SNAPSHOT_FULL = "full"
|
||||
SNAPSHOT_PARTIAL = "partial"
|
||||
|
@ -15,17 +15,18 @@ from ..addons.build import AddonBuild
|
||||
from ..const import (
|
||||
ENV_TIME,
|
||||
ENV_TOKEN,
|
||||
ENV_TOKEN_OLD,
|
||||
ENV_TOKEN_HASSIO,
|
||||
MAP_ADDONS,
|
||||
MAP_BACKUP,
|
||||
MAP_CONFIG,
|
||||
MAP_MEDIA,
|
||||
MAP_SHARE,
|
||||
MAP_SSL,
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
)
|
||||
from ..coresys import CoreSys
|
||||
from ..exceptions import DockerAPIError
|
||||
from ..exceptions import CoreDNSError, DockerAPIError
|
||||
from ..utils import process_lock
|
||||
from .interface import DockerInterface
|
||||
|
||||
@ -118,7 +119,7 @@ class DockerAddon(DockerInterface):
|
||||
**addon_env,
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.addon.supervisor_token,
|
||||
ENV_TOKEN_OLD: self.addon.supervisor_token,
|
||||
ENV_TOKEN_HASSIO: self.addon.supervisor_token,
|
||||
}
|
||||
|
||||
@property
|
||||
@ -269,6 +270,16 @@ class DockerAddon(DockerInterface):
|
||||
}
|
||||
)
|
||||
|
||||
if MAP_MEDIA in addon_mapping:
|
||||
volumes.update(
|
||||
{
|
||||
str(self.sys_config.path_extern_media): {
|
||||
"bind": "/media",
|
||||
"mode": addon_mapping[MAP_MEDIA],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Init other hardware mappings
|
||||
|
||||
# GPIO support
|
||||
@ -368,7 +379,13 @@ class DockerAddon(DockerInterface):
|
||||
_LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version)
|
||||
|
||||
# Write data to DNS server
|
||||
self.sys_plugins.dns.add_host(ipv4=self.ip_address, names=[self.addon.hostname])
|
||||
try:
|
||||
self.sys_plugins.dns.add_host(
|
||||
ipv4=self.ip_address, names=[self.addon.hostname]
|
||||
)
|
||||
except CoreDNSError as err:
|
||||
_LOGGER.warning("Can't update DNS for %s", self.name)
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
def _install(
|
||||
self, tag: str, image: Optional[str] = None, latest: bool = False
|
||||
@ -494,5 +511,9 @@ class DockerAddon(DockerInterface):
|
||||
Need run inside executor.
|
||||
"""
|
||||
if self.ip_address != NO_ADDDRESS:
|
||||
try:
|
||||
self.sys_plugins.dns.delete_host(self.addon.hostname)
|
||||
except CoreDNSError as err:
|
||||
_LOGGER.warning("Can't update DNS for %s", self.name)
|
||||
self.sys_capture_exception(err)
|
||||
super()._stop(remove_container)
|
||||
|
@ -45,7 +45,10 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
||||
extra_hosts={
|
||||
"supervisor": self.sys_docker.network.supervisor,
|
||||
"observer": self.sys_docker.network.observer,
|
||||
},
|
||||
environment={
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.sys_plugins.cli.supervisor_token,
|
||||
|
@ -6,7 +6,7 @@ from typing import Awaitable, Dict, Optional
|
||||
import docker
|
||||
import requests
|
||||
|
||||
from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_OLD, LABEL_MACHINE, MACHINE_ID
|
||||
from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_HASSIO, LABEL_MACHINE, MACHINE_ID
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import CommandReturn, DockerInterface
|
||||
|
||||
@ -62,6 +62,10 @@ class DockerHomeAssistant(DockerInterface):
|
||||
"bind": "/share",
|
||||
"mode": "rw",
|
||||
},
|
||||
str(self.sys_config.path_extern_media): {
|
||||
"bind": "/media",
|
||||
"mode": "rw",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -111,12 +115,16 @@ class DockerHomeAssistant(DockerInterface):
|
||||
init=False,
|
||||
network_mode="host",
|
||||
volumes=self.volumes,
|
||||
extra_hosts={
|
||||
"supervisor": self.sys_docker.network.supervisor,
|
||||
"observer": self.sys_docker.network.observer,
|
||||
},
|
||||
environment={
|
||||
"HASSIO": self.sys_docker.network.supervisor,
|
||||
"SUPERVISOR": self.sys_docker.network.supervisor,
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.sys_homeassistant.supervisor_token,
|
||||
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
|
||||
ENV_TOKEN_HASSIO: self.sys_homeassistant.supervisor_token,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -334,7 +334,13 @@ class DockerInterface(CoreSysAttributes):
|
||||
raise DockerAPIError() from err
|
||||
|
||||
# Cleanup Current
|
||||
for image in self.sys_docker.images.list(name=self.image):
|
||||
try:
|
||||
images_list = self.sys_docker.images.list(name=self.image)
|
||||
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||
_LOGGER.waring("Corrupt docker overlayfs found: %s", err)
|
||||
raise DockerAPIError() from err
|
||||
|
||||
for image in images_list:
|
||||
if origin.id == image.id:
|
||||
continue
|
||||
|
||||
@ -346,7 +352,13 @@ class DockerInterface(CoreSysAttributes):
|
||||
if not old_image or self.image == old_image:
|
||||
return
|
||||
|
||||
for image in self.sys_docker.images.list(name=old_image):
|
||||
try:
|
||||
images_list = self.sys_docker.images.list(name=old_image)
|
||||
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||
_LOGGER.waring("Corrupt docker overlayfs found: %s", err)
|
||||
raise DockerAPIError() from err
|
||||
|
||||
for image in images_list:
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
_LOGGER.info("Cleanup images: %s", image.tags)
|
||||
self.sys_docker.images.remove(image.id, force=True)
|
||||
|
@ -69,6 +69,11 @@ class DockerNetwork:
|
||||
"""Return cli of the network."""
|
||||
return DOCKER_NETWORK_MASK[5]
|
||||
|
||||
@property
|
||||
def observer(self) -> IPv4Address:
|
||||
"""Return observer of the network."""
|
||||
return DOCKER_NETWORK_MASK[6]
|
||||
|
||||
def _get_network(self) -> docker.models.networks.Network:
|
||||
"""Get supervisor network."""
|
||||
try:
|
||||
|
62
supervisor/docker/observer.py
Normal file
62
supervisor/docker/observer.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""Observer docker object."""
|
||||
import logging
|
||||
|
||||
from ..const import ENV_TIME, ENV_TOKEN
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
OBSERVER_DOCKER_NAME: str = "hassio_observer"
|
||||
|
||||
|
||||
class DockerObserver(DockerInterface, CoreSysAttributes):
|
||||
"""Docker Supervisor wrapper for observer plugin."""
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
"""Return name of observer image."""
|
||||
return self.sys_plugins.observer.image
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of Docker container."""
|
||||
return OBSERVER_DOCKER_NAME
|
||||
|
||||
def _run(self) -> None:
|
||||
"""Run Docker image.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
if self._is_running():
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
self.image,
|
||||
version=self.sys_plugins.observer.version,
|
||||
init=False,
|
||||
ipv4=self.sys_docker.network.observer,
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
restart_policy={"Name": "always"},
|
||||
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
||||
environment={
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.sys_plugins.observer.supervisor_token,
|
||||
},
|
||||
volumes={"/run/docker.sock": {"bind": "/run/docker.sock", "mode": "ro"}},
|
||||
ports={"80/tcp": 4357},
|
||||
)
|
||||
|
||||
self._meta = docker_container.attrs
|
||||
_LOGGER.info(
|
||||
"Start Observer %s with version %s - %s",
|
||||
self.image,
|
||||
self.version,
|
||||
self.sys_docker.network.observer,
|
||||
)
|
@ -65,6 +65,17 @@ class CliUpdateError(CliError):
|
||||
"""Error on update of a HA cli."""
|
||||
|
||||
|
||||
# Observer
|
||||
|
||||
|
||||
class ObserverError(HassioError):
|
||||
"""General Observer exception."""
|
||||
|
||||
|
||||
class ObserverUpdateError(ObserverError):
|
||||
"""Error on update of a Observer."""
|
||||
|
||||
|
||||
# Multicast
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@ from ..exceptions import (
|
||||
CoreDNSError,
|
||||
HomeAssistantError,
|
||||
MulticastError,
|
||||
ObserverError,
|
||||
)
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -22,6 +23,7 @@ RUN_UPDATE_CLI = 28100
|
||||
RUN_UPDATE_DNS = 30100
|
||||
RUN_UPDATE_AUDIO = 30200
|
||||
RUN_UPDATE_MULTICAST = 30300
|
||||
RUN_UPDATE_OBSERVER = 30400
|
||||
|
||||
RUN_RELOAD_ADDONS = 10800
|
||||
RUN_RELOAD_SNAPSHOTS = 72000
|
||||
@ -35,6 +37,7 @@ RUN_WATCHDOG_HOMEASSISTANT_API = 120
|
||||
RUN_WATCHDOG_DNS_DOCKER = 30
|
||||
RUN_WATCHDOG_AUDIO_DOCKER = 60
|
||||
RUN_WATCHDOG_CLI_DOCKER = 60
|
||||
RUN_WATCHDOG_OBSERVER_DOCKER = 60
|
||||
RUN_WATCHDOG_MULTICAST_DOCKER = 60
|
||||
|
||||
RUN_WATCHDOG_ADDON_DOCKER = 30
|
||||
@ -60,6 +63,7 @@ class Tasks(CoreSysAttributes):
|
||||
self.sys_scheduler.register_task(self._update_dns, RUN_UPDATE_DNS)
|
||||
self.sys_scheduler.register_task(self._update_audio, RUN_UPDATE_AUDIO)
|
||||
self.sys_scheduler.register_task(self._update_multicast, RUN_UPDATE_MULTICAST)
|
||||
self.sys_scheduler.register_task(self._update_observer, RUN_UPDATE_OBSERVER)
|
||||
|
||||
# Reload
|
||||
self.sys_scheduler.register_task(self.sys_store.reload, RUN_RELOAD_ADDONS)
|
||||
@ -86,6 +90,9 @@ class Tasks(CoreSysAttributes):
|
||||
self.sys_scheduler.register_task(
|
||||
self._watchdog_cli_docker, RUN_WATCHDOG_CLI_DOCKER
|
||||
)
|
||||
self.sys_scheduler.register_task(
|
||||
self._watchdog_observer_docker, RUN_WATCHDOG_OBSERVER_DOCKER
|
||||
)
|
||||
self.sys_scheduler.register_task(
|
||||
self._watchdog_multicast_docker, RUN_WATCHDOG_MULTICAST_DOCKER
|
||||
)
|
||||
@ -225,6 +232,14 @@ class Tasks(CoreSysAttributes):
|
||||
_LOGGER.info("Found new PulseAudio plugin version")
|
||||
await self.sys_plugins.audio.update()
|
||||
|
||||
async def _update_observer(self):
|
||||
"""Check and run update of Observer plugin."""
|
||||
if not self.sys_plugins.observer.need_update:
|
||||
return
|
||||
|
||||
_LOGGER.info("Found new Observer plugin version")
|
||||
await self.sys_plugins.observer.update()
|
||||
|
||||
async def _update_multicast(self):
|
||||
"""Check and run update of multicast."""
|
||||
if not self.sys_plugins.multicast.need_update:
|
||||
@ -278,6 +293,21 @@ class Tasks(CoreSysAttributes):
|
||||
except CliError:
|
||||
_LOGGER.error("Watchdog cli reanimation failed!")
|
||||
|
||||
async def _watchdog_observer_docker(self):
|
||||
"""Check running state of Docker and start if they is close."""
|
||||
# if observer plugin is active
|
||||
if (
|
||||
await self.sys_plugins.observer.is_running()
|
||||
or self.sys_plugins.observer.in_progress
|
||||
):
|
||||
return
|
||||
_LOGGER.warning("Watchdog found a problem with observer plugin!")
|
||||
|
||||
try:
|
||||
await self.sys_plugins.observer.start()
|
||||
except ObserverError:
|
||||
_LOGGER.error("Watchdog observer reanimation failed!")
|
||||
|
||||
async def _watchdog_multicast_docker(self):
|
||||
"""Check running state of Docker and start if they is close."""
|
||||
# if multicast plugin is active
|
||||
|
@ -10,6 +10,7 @@ from .audio import Audio
|
||||
from .cli import HaCli
|
||||
from .dns import CoreDNS
|
||||
from .multicast import Multicast
|
||||
from .observer import Observer
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -20,6 +21,7 @@ class PluginManager(CoreSysAttributes):
|
||||
required_cli: LegacyVersion = pkg_parse("26")
|
||||
required_dns: LegacyVersion = pkg_parse("9")
|
||||
required_audio: LegacyVersion = pkg_parse("17")
|
||||
required_observer: LegacyVersion = pkg_parse("2")
|
||||
required_multicast: LegacyVersion = pkg_parse("3")
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
@ -29,6 +31,7 @@ class PluginManager(CoreSysAttributes):
|
||||
self._cli: HaCli = HaCli(coresys)
|
||||
self._dns: CoreDNS = CoreDNS(coresys)
|
||||
self._audio: Audio = Audio(coresys)
|
||||
self._observer: Observer = Observer(coresys)
|
||||
self._multicast: Multicast = Multicast(coresys)
|
||||
|
||||
@property
|
||||
@ -46,6 +49,11 @@ class PluginManager(CoreSysAttributes):
|
||||
"""Return audio handler."""
|
||||
return self._audio
|
||||
|
||||
@property
|
||||
def observer(self) -> Observer:
|
||||
"""Return observer handler."""
|
||||
return self._observer
|
||||
|
||||
@property
|
||||
def multicast(self) -> Multicast:
|
||||
"""Return multicast handler."""
|
||||
@ -58,6 +66,7 @@ class PluginManager(CoreSysAttributes):
|
||||
self.dns,
|
||||
self.audio,
|
||||
self.cli,
|
||||
self.observer,
|
||||
self.multicast,
|
||||
):
|
||||
try:
|
||||
@ -71,6 +80,7 @@ class PluginManager(CoreSysAttributes):
|
||||
(self._audio, self.required_audio),
|
||||
(self._dns, self.required_dns),
|
||||
(self._cli, self.required_cli),
|
||||
(self._observer, self.required_observer),
|
||||
(self._multicast, self.required_multicast),
|
||||
):
|
||||
# Check if need an update
|
||||
@ -109,6 +119,7 @@ class PluginManager(CoreSysAttributes):
|
||||
self.dns.repair(),
|
||||
self.audio.repair(),
|
||||
self.cli.repair(),
|
||||
self.observer.repair(),
|
||||
self.multicast.repair(),
|
||||
]
|
||||
)
|
||||
|
@ -11,12 +11,13 @@ from typing import Awaitable, Optional
|
||||
|
||||
import jinja2
|
||||
|
||||
from ..const import ATTR_IMAGE, ATTR_VERSION, FILE_HASSIO_AUDIO
|
||||
from ..const import ATTR_IMAGE, ATTR_VERSION
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.audio import DockerAudio
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import AudioError, AudioUpdateError, DockerAPIError
|
||||
from ..utils.json import JsonConfig
|
||||
from .const import FILE_HASSIO_AUDIO
|
||||
from .validate import SCHEMA_AUDIO_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -225,8 +226,9 @@ class Audio(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.info("Repair Audio %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version)
|
||||
except DockerAPIError:
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Repairing of Audio failed")
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
def pulse_client(self, input_profile=None, output_profile=None) -> str:
|
||||
"""Generate an /etc/pulse/client.conf data."""
|
||||
|
@ -8,12 +8,13 @@ import logging
|
||||
import secrets
|
||||
from typing import Awaitable, Optional
|
||||
|
||||
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_VERSION, FILE_HASSIO_CLI
|
||||
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_VERSION
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.cli import DockerCli
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import CliError, CliUpdateError, DockerAPIError
|
||||
from ..utils.json import JsonConfig
|
||||
from .const import FILE_HASSIO_CLI
|
||||
from .validate import SCHEMA_CLI_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -90,7 +91,7 @@ class HaCli(CoreSysAttributes, JsonConfig):
|
||||
self.image = self.instance.image
|
||||
self.save_data()
|
||||
|
||||
# Run PulseAudio
|
||||
# Run CLI
|
||||
with suppress(CliError):
|
||||
if not await self.instance.is_running():
|
||||
await self.start()
|
||||
@ -192,5 +193,6 @@ class HaCli(CoreSysAttributes, JsonConfig):
|
||||
_LOGGER.info("Repair HA cli %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version, latest=True)
|
||||
except DockerAPIError:
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Repairing of HA cli failed")
|
||||
self.sys_capture_exception(err)
|
||||
|
10
supervisor/plugins/const.py
Normal file
10
supervisor/plugins/const.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Const for plugins."""
|
||||
from pathlib import Path
|
||||
|
||||
from ..const import SUPERVISOR_DATA
|
||||
|
||||
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
||||
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
||||
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
||||
FILE_HASSIO_OBSERVER = Path(SUPERVISOR_DATA, "observer.json")
|
||||
FILE_HASSIO_MULTICAST = Path(SUPERVISOR_DATA, "multicast.json")
|
@ -13,20 +13,14 @@ import attr
|
||||
import jinja2
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import (
|
||||
ATTR_IMAGE,
|
||||
ATTR_SERVERS,
|
||||
ATTR_VERSION,
|
||||
DNS_SUFFIX,
|
||||
FILE_HASSIO_DNS,
|
||||
LogLevel,
|
||||
)
|
||||
from ..const import ATTR_IMAGE, ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, LogLevel
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.dns import DockerDNS
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError
|
||||
from ..utils.json import JsonConfig
|
||||
from ..validate import dns_url
|
||||
from .const import FILE_HASSIO_DNS
|
||||
from .validate import SCHEMA_DNS_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -322,6 +316,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
||||
write=False,
|
||||
)
|
||||
self.add_host(self.sys_docker.network.dns, ["dns"], write=False)
|
||||
self.add_host(self.sys_docker.network.observer, ["observer"], write=False)
|
||||
|
||||
def write_hosts(self) -> None:
|
||||
"""Write hosts from memory to file."""
|
||||
@ -419,8 +414,9 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.info("Repair CoreDNS %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version)
|
||||
except DockerAPIError:
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Repairing of CoreDNS failed")
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
def _write_resolv(self, resolv_conf: Path) -> None:
|
||||
"""Update/Write resolv.conf file."""
|
||||
|
@ -7,12 +7,13 @@ from contextlib import suppress
|
||||
import logging
|
||||
from typing import Awaitable, Optional
|
||||
|
||||
from ..const import ATTR_IMAGE, ATTR_VERSION, FILE_HASSIO_MULTICAST
|
||||
from ..const import ATTR_IMAGE, ATTR_VERSION
|
||||
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 .const import FILE_HASSIO_MULTICAST
|
||||
from .validate import SCHEMA_MULTICAST_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -205,5 +206,6 @@ class Multicast(JsonConfig, CoreSysAttributes):
|
||||
_LOGGER.info("Repair Multicast %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version)
|
||||
except DockerAPIError:
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Repairing of Multicast failed")
|
||||
self.sys_capture_exception(err)
|
||||
|
188
supervisor/plugins/observer.py
Normal file
188
supervisor/plugins/observer.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Home Assistant observer plugin.
|
||||
|
||||
Code: https://github.com/home-assistant/plugin-observer
|
||||
"""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Awaitable, Optional
|
||||
|
||||
from ..const import ATTR_ACCESS_TOKEN, ATTR_IMAGE, ATTR_VERSION
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.observer import DockerObserver
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import DockerAPIError, ObserverError, ObserverUpdateError
|
||||
from ..utils.json import JsonConfig
|
||||
from .const import FILE_HASSIO_OBSERVER
|
||||
from .validate import SCHEMA_OBSERVER_CONFIG
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Observer(CoreSysAttributes, JsonConfig):
|
||||
"""Supervisor observer instance."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
"""Initialize observer handler."""
|
||||
super().__init__(FILE_HASSIO_OBSERVER, SCHEMA_OBSERVER_CONFIG)
|
||||
self.coresys: CoreSys = coresys
|
||||
self.instance: DockerObserver = DockerObserver(coresys)
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
"""Return version of observer."""
|
||||
return self._data.get(ATTR_VERSION)
|
||||
|
||||
@version.setter
|
||||
def version(self, value: str) -> None:
|
||||
"""Set current version of observer."""
|
||||
self._data[ATTR_VERSION] = value
|
||||
|
||||
@property
|
||||
def image(self) -> str:
|
||||
"""Return current image of observer."""
|
||||
if self._data.get(ATTR_IMAGE):
|
||||
return self._data[ATTR_IMAGE]
|
||||
return f"homeassistant/{self.sys_arch.supervisor}-hassio-observer"
|
||||
|
||||
@image.setter
|
||||
def image(self, value: str) -> None:
|
||||
"""Return current image of observer."""
|
||||
self._data[ATTR_IMAGE] = value
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str:
|
||||
"""Return version of latest observer."""
|
||||
return self.sys_updater.version_observer
|
||||
|
||||
@property
|
||||
def need_update(self) -> bool:
|
||||
"""Return true if a observer update is available."""
|
||||
return self.version != self.latest_version
|
||||
|
||||
@property
|
||||
def supervisor_token(self) -> str:
|
||||
"""Return an access token for the Observer API."""
|
||||
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Return True if a task is in progress."""
|
||||
return self.instance.in_progress
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Load observer setup."""
|
||||
# Check observer state
|
||||
try:
|
||||
# Evaluate Version if we lost this information
|
||||
if not self.version:
|
||||
self.version = await self.instance.get_latest_version()
|
||||
|
||||
await self.instance.attach(tag=self.version)
|
||||
except DockerAPIError:
|
||||
_LOGGER.info(
|
||||
"No observer plugin Docker image %s found.", self.instance.image
|
||||
)
|
||||
|
||||
# Install observer
|
||||
with suppress(ObserverError):
|
||||
await self.install()
|
||||
else:
|
||||
self.version = self.instance.version
|
||||
self.image = self.instance.image
|
||||
self.save_data()
|
||||
|
||||
# Run Observer
|
||||
with suppress(ObserverError):
|
||||
if not await self.instance.is_running():
|
||||
await self.start()
|
||||
|
||||
async def install(self) -> None:
|
||||
"""Install observer."""
|
||||
_LOGGER.info("Setup observer plugin")
|
||||
while True:
|
||||
# read observer 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_observer
|
||||
)
|
||||
break
|
||||
_LOGGER.warning("Error on install observer plugin. Retry in 30sec")
|
||||
await asyncio.sleep(30)
|
||||
|
||||
_LOGGER.info("observer plugin now installed")
|
||||
self.version = self.instance.version
|
||||
self.image = self.sys_updater.image_observer
|
||||
self.save_data()
|
||||
|
||||
async def update(self, version: Optional[str] = None) -> None:
|
||||
"""Update local HA observer."""
|
||||
version = version or self.latest_version
|
||||
old_image = self.image
|
||||
|
||||
if version == self.version:
|
||||
_LOGGER.warning("Version %s is already installed for observer", version)
|
||||
return
|
||||
|
||||
try:
|
||||
await self.instance.update(version, image=self.sys_updater.image_observer)
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("HA observer update failed")
|
||||
raise ObserverUpdateError() from err
|
||||
else:
|
||||
self.version = version
|
||||
self.image = self.sys_updater.image_observer
|
||||
self.save_data()
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
await self.instance.cleanup(old_image=old_image)
|
||||
|
||||
# Start observer
|
||||
await self.start()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Run observer."""
|
||||
# Create new API token
|
||||
if not self.supervisor_token:
|
||||
self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
|
||||
self.save_data()
|
||||
|
||||
# Start Instance
|
||||
_LOGGER.info("Start observer plugin")
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Can't start observer plugin")
|
||||
raise ObserverError() from err
|
||||
|
||||
async def stats(self) -> DockerStats:
|
||||
"""Return stats of observer."""
|
||||
try:
|
||||
return await self.instance.stats()
|
||||
except DockerAPIError as err:
|
||||
raise ObserverError() from err
|
||||
|
||||
def is_running(self) -> Awaitable[bool]:
|
||||
"""Return True if Docker container is running.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self.instance.is_running()
|
||||
|
||||
async def repair(self) -> None:
|
||||
"""Repair observer container."""
|
||||
if await self.instance.exists():
|
||||
return
|
||||
|
||||
_LOGGER.info("Repair HA observer %s", self.version)
|
||||
try:
|
||||
await self.instance.install(self.version)
|
||||
except DockerAPIError as err:
|
||||
_LOGGER.error("Repairing of HA observer failed")
|
||||
self.sys_capture_exception(err)
|
@ -35,3 +35,13 @@ SCHEMA_MULTICAST_CONFIG = vol.Schema(
|
||||
{vol.Optional(ATTR_VERSION): version_tag, vol.Optional(ATTR_IMAGE): docker_image},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_OBSERVER_CONFIG = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_VERSION): version_tag,
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(ATTR_ACCESS_TOKEN): token,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
@ -26,6 +26,7 @@ from ..const import (
|
||||
CRYPTO_AES128,
|
||||
FOLDER_ADDONS,
|
||||
FOLDER_HOMEASSISTANT,
|
||||
FOLDER_MEDIA,
|
||||
FOLDER_SHARE,
|
||||
FOLDER_SSL,
|
||||
SNAPSHOT_FULL,
|
||||
@ -33,7 +34,13 @@ from ..const import (
|
||||
)
|
||||
from ..validate import docker_image, network_port, repositories, version_tag
|
||||
|
||||
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
|
||||
ALL_FOLDERS = [
|
||||
FOLDER_HOMEASSISTANT,
|
||||
FOLDER_SHARE,
|
||||
FOLDER_ADDONS,
|
||||
FOLDER_SSL,
|
||||
FOLDER_MEDIA,
|
||||
]
|
||||
|
||||
|
||||
def unique_addons(addons_list):
|
||||
|
@ -17,6 +17,7 @@ from .const import (
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_IMAGE,
|
||||
ATTR_MULTICAST,
|
||||
ATTR_OBSERVER,
|
||||
ATTR_SUPERVISOR,
|
||||
FILE_HASSIO_UPDATER,
|
||||
URL_HASSIO_VERSION,
|
||||
@ -79,6 +80,11 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
"""Return latest version of Audio."""
|
||||
return self._data.get(ATTR_AUDIO)
|
||||
|
||||
@property
|
||||
def version_observer(self) -> Optional[str]:
|
||||
"""Return latest version of Observer."""
|
||||
return self._data.get(ATTR_OBSERVER)
|
||||
|
||||
@property
|
||||
def version_multicast(self) -> Optional[str]:
|
||||
"""Return latest version of Multicast."""
|
||||
@ -123,6 +129,15 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
return None
|
||||
return self._data[ATTR_IMAGE][ATTR_AUDIO].format(arch=self.sys_arch.supervisor)
|
||||
|
||||
@property
|
||||
def image_observer(self) -> Optional[str]:
|
||||
"""Return latest version of Observer."""
|
||||
if ATTR_OBSERVER not in self._data[ATTR_IMAGE]:
|
||||
return None
|
||||
return self._data[ATTR_IMAGE][ATTR_OBSERVER].format(
|
||||
arch=self.sys_arch.supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def image_multicast(self) -> Optional[str]:
|
||||
"""Return latest version of Multicast."""
|
||||
@ -184,6 +199,7 @@ class Updater(JsonConfig, CoreSysAttributes):
|
||||
self._data[ATTR_CLI] = data["cli"]
|
||||
self._data[ATTR_DNS] = data["dns"]
|
||||
self._data[ATTR_AUDIO] = data["audio"]
|
||||
self._data[ATTR_OBSERVER] = data["observer"]
|
||||
self._data[ATTR_MULTICAST] = data["multicast"]
|
||||
|
||||
# Update images for that versions
|
||||
@ -192,6 +208,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_OBSERVER] = data["image"]["observer"]
|
||||
self._data[ATTR_IMAGE][ATTR_MULTICAST] = data["image"]["multicast"]
|
||||
|
||||
except KeyError as err:
|
||||
|
@ -33,12 +33,20 @@ RE_GVARIANT_STRING_ESC: re.Pattern[Any] = re.compile(
|
||||
RE_GVARIANT_STRING: re.Pattern[Any] = re.compile(
|
||||
r"(?<=(?: |{|\[|\(|<))'(.*?)'(?=(?:|]|}|,|\)|>))"
|
||||
)
|
||||
RE_GVARIANT_BINARY: re.Pattern[Any] = re.compile(r"\[byte (.*?)\]")
|
||||
RE_GVARIANT_BINARY: re.Pattern[Any] = re.compile(
|
||||
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|\[byte (.*?)\]"
|
||||
)
|
||||
RE_GVARIANT_BINARY_STRING: re.Pattern[Any] = re.compile(
|
||||
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|<?b\'(.*?)\'>?"
|
||||
)
|
||||
RE_GVARIANT_TUPLE_O: re.Pattern[Any] = re.compile(r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(\()")
|
||||
RE_GVARIANT_TUPLE_C: re.Pattern[Any] = re.compile(
|
||||
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(,?\))"
|
||||
)
|
||||
|
||||
RE_BIN_STRING_OCT: re.Pattern[Any] = re.compile(r"\\\\(\d{3})")
|
||||
RE_BIN_STRING_HEX: re.Pattern[Any] = re.compile(r"\\\\x(\d{2})")
|
||||
|
||||
RE_MONITOR_OUTPUT: re.Pattern[Any] = re.compile(r".+?: (?P<signal>[^ ].+) (?P<data>.*)")
|
||||
|
||||
# Map GDBus to errors
|
||||
@ -66,6 +74,13 @@ def _convert_bytes(value: str) -> str:
|
||||
return f"[{', '.join(str(char) for char in data)}]"
|
||||
|
||||
|
||||
def _convert_bytes_string(value: str) -> str:
|
||||
"""Convert bytes to string or byte-array."""
|
||||
data = RE_BIN_STRING_OCT.sub(lambda x: chr(int(x.group(1), 8)), value)
|
||||
data = RE_BIN_STRING_HEX.sub(lambda x: chr(int(f"0x{x.group(1)}", 0)), data)
|
||||
return f"[{', '.join(str(char) for char in list(char for char in data.encode()))}]"
|
||||
|
||||
|
||||
class DBus:
|
||||
"""DBus handler."""
|
||||
|
||||
@ -120,15 +135,23 @@ class DBus:
|
||||
def parse_gvariant(raw: str) -> Any:
|
||||
"""Parse GVariant input to python."""
|
||||
# Process first string
|
||||
json_raw = RE_GVARIANT_BINARY.sub(
|
||||
lambda x: _convert_bytes(x.group(1)),
|
||||
raw,
|
||||
)
|
||||
json_raw = RE_GVARIANT_STRING_ESC.sub(
|
||||
lambda x: x.group(0).replace('"', '\\"'), json_raw
|
||||
lambda x: x.group(0).replace('"', '\\"'), raw
|
||||
)
|
||||
json_raw = RE_GVARIANT_STRING.sub(r'"\1"', json_raw)
|
||||
|
||||
# Handle Bytes
|
||||
json_raw = RE_GVARIANT_BINARY.sub(
|
||||
lambda x: x.group(0) if not x.group(1) else _convert_bytes(x.group(1)),
|
||||
json_raw,
|
||||
)
|
||||
json_raw = RE_GVARIANT_BINARY_STRING.sub(
|
||||
lambda x: x.group(0)
|
||||
if not x.group(1)
|
||||
else _convert_bytes_string(x.group(1)),
|
||||
json_raw,
|
||||
)
|
||||
|
||||
# Remove complex type handling
|
||||
json_raw: str = RE_GVARIANT_TYPE.sub(
|
||||
lambda x: x.group(0) if not x.group(1) else "", json_raw
|
||||
|
@ -26,6 +26,7 @@ from .const import (
|
||||
ATTR_LAST_BOOT,
|
||||
ATTR_LOGGING,
|
||||
ATTR_MULTICAST,
|
||||
ATTR_OBSERVER,
|
||||
ATTR_PORT,
|
||||
ATTR_PORTS,
|
||||
ATTR_REFRESH_TOKEN,
|
||||
@ -74,9 +75,13 @@ def dns_url(url: str) -> str:
|
||||
raise vol.Invalid("Doesn't start with dns://") from None
|
||||
address: str = url[6:] # strip the dns:// off
|
||||
try:
|
||||
ipaddress.ip_address(address) # matches ipv4 or ipv6 addresses
|
||||
ip = ipaddress.ip_address(address) # matches ipv4 or ipv6 addresses
|
||||
except ValueError:
|
||||
raise vol.Invalid(f"Invalid DNS URL: {url}") from None
|
||||
|
||||
# Currently only IPv4 work with docker network
|
||||
if ip.version != 4:
|
||||
raise vol.Invalid(f"Only IPv4 is working for DNS: {url}") from None
|
||||
return url
|
||||
|
||||
|
||||
@ -137,6 +142,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_CLI): vol.All(version_tag, str),
|
||||
vol.Optional(ATTR_DNS): vol.All(version_tag, str),
|
||||
vol.Optional(ATTR_AUDIO): vol.All(version_tag, str),
|
||||
vol.Optional(ATTR_OBSERVER): vol.All(version_tag, str),
|
||||
vol.Optional(ATTR_MULTICAST): vol.All(version_tag, str),
|
||||
vol.Optional(ATTR_IMAGE, default=dict): vol.Schema(
|
||||
{
|
||||
@ -145,6 +151,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_OBSERVER): docker_image,
|
||||
vol.Optional(ATTR_MULTICAST): docker_image,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
|
@ -26,6 +26,7 @@ async def test_dns_url_v4_good():
|
||||
|
||||
def test_dns_url_v6_good():
|
||||
"""Test the DNS validator with known-good ipv6 DNS URLs."""
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
for url in DNS_GOOD_V6:
|
||||
assert validate.dns_url(url)
|
||||
|
||||
@ -37,6 +38,7 @@ def test_dns_server_list_v4():
|
||||
|
||||
def test_dns_server_list_v6():
|
||||
"""Test a list with v6 addresses."""
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
assert validate.dns_server_list(DNS_GOOD_V6)
|
||||
|
||||
|
||||
@ -44,9 +46,11 @@ def test_dns_server_list_combined():
|
||||
"""Test a list with both v4 and v6 addresses."""
|
||||
combined = DNS_GOOD_V4 + DNS_GOOD_V6
|
||||
# test the matches
|
||||
assert validate.dns_server_list(combined)
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate.dns_server_list(combined)
|
||||
# test max_length is OK still
|
||||
assert validate.dns_server_list(combined)
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate.dns_server_list(combined)
|
||||
# test that it fails when the list is too long
|
||||
with pytest.raises(vol.error.Invalid):
|
||||
validate.dns_server_list(combined + combined + combined + combined)
|
||||
@ -72,6 +76,7 @@ def test_version_complex():
|
||||
"""Test version simple with good version."""
|
||||
for version in (
|
||||
"landingpage",
|
||||
"dev",
|
||||
"1c002dd",
|
||||
"1.1.1",
|
||||
"1.0",
|
||||
|
@ -404,6 +404,54 @@ def test_networkmanager_binary_data():
|
||||
]
|
||||
|
||||
|
||||
def test_networkmanager_binary_string_data():
|
||||
"""Test NetworkManager Binary string datastrings."""
|
||||
raw = "({'802-11-wireless': {'mac-address-blacklist': <@as []>, 'mac-address': <b'*~_\\\\035\\\\311'>, 'mode': <'infrastructure'>, 'security': <'802-11-wireless-security'>, 'seen-bssids': <['7C:2E:BD:98:1B:06']>, 'ssid': <[byte 0x4e, 0x45, 0x54, 0x54]>}, 'connection': {'id': <'NETT'>, 'interface-name': <'wlan0'>, 'permissions': <@as []>, 'timestamp': <uint64 1598526799>, 'type': <'802-11-wireless'>, 'uuid': <'13f9af79-a6e9-4e07-9353-165ad57bf1a8'>}, 'ipv6': {'address-data': <@aa{sv} []>, 'addresses': <@a(ayuay) []>, 'dns': <@aay []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@a(ayuayu) []>}, '802-11-wireless-security': {'auth-alg': <'open'>, 'key-mgmt': <'wpa-psk'>}, 'ipv4': {'address-data': <@aa{sv} []>, 'addresses': <@aau []>, 'dns': <@au []>, 'dns-search': <@as []>, 'method': <'auto'>, 'route-data': <@aa{sv} []>, 'routes': <@aau []>}, 'proxy': {}},)"
|
||||
|
||||
data = DBus.parse_gvariant(raw)
|
||||
|
||||
assert data == [
|
||||
{
|
||||
"802-11-wireless": {
|
||||
"mac-address": [42, 126, 95, 29, 195, 137],
|
||||
"mac-address-blacklist": [],
|
||||
"mode": "infrastructure",
|
||||
"security": "802-11-wireless-security",
|
||||
"seen-bssids": ["7C:2E:BD:98:1B:06"],
|
||||
"ssid": [78, 69, 84, 84],
|
||||
},
|
||||
"802-11-wireless-security": {"auth-alg": "open", "key-mgmt": "wpa-psk"},
|
||||
"connection": {
|
||||
"id": "NETT",
|
||||
"interface-name": "wlan0",
|
||||
"permissions": [],
|
||||
"timestamp": 1598526799,
|
||||
"type": "802-11-wireless",
|
||||
"uuid": "13f9af79-a6e9-4e07-9353-165ad57bf1a8",
|
||||
},
|
||||
"ipv4": {
|
||||
"address-data": [],
|
||||
"addresses": [],
|
||||
"dns": [],
|
||||
"dns-search": [],
|
||||
"method": "auto",
|
||||
"route-data": [],
|
||||
"routes": [],
|
||||
},
|
||||
"ipv6": {
|
||||
"address-data": [],
|
||||
"addresses": [],
|
||||
"dns": [],
|
||||
"dns-search": [],
|
||||
"method": "auto",
|
||||
"route-data": [],
|
||||
"routes": [],
|
||||
},
|
||||
"proxy": {},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_v6():
|
||||
"""Test IPv6 Property."""
|
||||
raw = "({'addresses': <[([byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10], uint32 64, [byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])]>, 'dns': <[[byte 0x20, 0x01, 0x04, 0x70, 0x79, 0x2d, 0x00, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05]]>})"
|
||||
|
Loading…
x
Reference in New Issue
Block a user