mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-24 09:36:31 +00:00
commit
eba1d01fc2
15
API.md
15
API.md
@ -25,7 +25,7 @@ On success / Code 200:
|
||||
|
||||
To access the API you need use an authorization header with a `Bearer` token.
|
||||
|
||||
The token is available for add-ons and Home Assistant using the
|
||||
The token is available for add-ons and Home Assistant using the
|
||||
`SUPERVISOR_TOKEN` environment variable.
|
||||
|
||||
### Supervisor
|
||||
@ -242,13 +242,16 @@ return:
|
||||
|
||||
```json
|
||||
{
|
||||
"hostname": "hostname|null",
|
||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||
"operating_system": "HassOS XY|Ubuntu 16.4|null",
|
||||
"kernel": "4.15.7|null",
|
||||
"chassis": "specific|null",
|
||||
"cpe": "xy|null",
|
||||
"deployment": "stable|beta|dev|null",
|
||||
"cpe": "xy|null"
|
||||
"disk_total": 32.0,
|
||||
"disk_used": 30.0,
|
||||
"disk_free": 2.0,
|
||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||
"hostname": "hostname|null",
|
||||
"kernel": "4.15.7|null",
|
||||
"operating_system": "HassOS XY|Ubuntu 16.4|null"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -14,6 +14,6 @@ pulsectl==20.5.1
|
||||
pytz==2020.1
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.16.3
|
||||
sentry-sdk==0.16.4
|
||||
uvloop==0.14.0
|
||||
voluptuous==0.11.7
|
||||
|
@ -45,7 +45,7 @@ class AddonManager(CoreSysAttributes):
|
||||
"""Return a list of all installed add-ons."""
|
||||
return list(self.local.values())
|
||||
|
||||
def get(self, addon_slug: str) -> Optional[AnyAddon]:
|
||||
def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
|
||||
"""Return an add-on from slug.
|
||||
|
||||
Prio:
|
||||
@ -54,7 +54,9 @@ class AddonManager(CoreSysAttributes):
|
||||
"""
|
||||
if addon_slug in self.local:
|
||||
return self.local[addon_slug]
|
||||
return self.store.get(addon_slug)
|
||||
if not local_only:
|
||||
return self.store.get(addon_slug)
|
||||
return None
|
||||
|
||||
def from_token(self, token: str) -> Optional[Addon]:
|
||||
"""Return an add-on from Supervisor token."""
|
||||
@ -156,7 +158,12 @@ class AddonManager(CoreSysAttributes):
|
||||
raise AddonsError() from None
|
||||
else:
|
||||
self.local[slug] = addon
|
||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||
|
||||
# Reload ingress tokens
|
||||
if addon.with_ingress:
|
||||
await self.sys_ingress.reload()
|
||||
|
||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||
|
||||
async def uninstall(self, slug: str) -> None:
|
||||
"""Remove an add-on."""
|
||||
@ -188,7 +195,9 @@ class AddonManager(CoreSysAttributes):
|
||||
await self.sys_ingress.update_hass_panel(addon)
|
||||
|
||||
# Cleanup Ingress dynamic port assignment
|
||||
self.sys_ingress.del_dynamic_port(slug)
|
||||
if addon.with_ingress:
|
||||
self.sys_create_task(self.sys_ingress.reload())
|
||||
self.sys_ingress.del_dynamic_port(slug)
|
||||
|
||||
# Cleanup discovery data
|
||||
for message in self.sys_discovery.list_messages:
|
||||
@ -302,6 +311,7 @@ class AddonManager(CoreSysAttributes):
|
||||
|
||||
# Update ingress
|
||||
if addon.with_ingress:
|
||||
await self.sys_ingress.reload()
|
||||
with suppress(HomeAssistantAPIError):
|
||||
await self.sys_ingress.update_hass_panel(addon)
|
||||
|
||||
|
@ -43,6 +43,7 @@ from ..coresys import CoreSys
|
||||
from ..docker.addon import DockerAddon
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsNotSupportedError,
|
||||
DockerAPIError,
|
||||
@ -375,7 +376,7 @@ class Addon(AddonModel):
|
||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||
return
|
||||
|
||||
raise AddonsError()
|
||||
raise AddonConfigurationError()
|
||||
|
||||
async def remove_data(self) -> None:
|
||||
"""Remove add-on data."""
|
||||
|
@ -11,6 +11,9 @@ from ..const import (
|
||||
ATTR_CPE,
|
||||
ATTR_DEPLOYMENT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DISK_FREE,
|
||||
ATTR_DISK_TOTAL,
|
||||
ATTR_DISK_USED,
|
||||
ATTR_FEATURES,
|
||||
ATTR_HOSTNAME,
|
||||
ATTR_KERNEL,
|
||||
@ -39,11 +42,14 @@ class APIHost(CoreSysAttributes):
|
||||
return {
|
||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||
ATTR_CPE: self.sys_host.info.cpe,
|
||||
ATTR_FEATURES: self.sys_host.supperted_features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
||||
ATTR_DISK_USED: self.sys_host.info.used_space,
|
||||
ATTR_FEATURES: self.sys_host.supported_features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
}
|
||||
|
||||
@api_process
|
||||
|
@ -39,7 +39,7 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
ATTR_SUPPORTED: self.sys_supported,
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_LOGGING: self.sys_config.logging,
|
||||
ATTR_TIMEZONE: self.sys_timezone,
|
||||
|
@ -100,7 +100,7 @@ class APISupervisor(CoreSysAttributes):
|
||||
ATTR_VERSION_LATEST: self.sys_updater.version_supervisor,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_ARCH: self.sys_supervisor.arch,
|
||||
ATTR_SUPPORTED: self.sys_supported,
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_HEALTHY: self.sys_core.healthy,
|
||||
ATTR_IP_ADDRESS: str(self.sys_supervisor.ip_address),
|
||||
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
||||
|
@ -16,13 +16,13 @@ from .arch import CpuArch
|
||||
from .auth import Auth
|
||||
from .const import (
|
||||
ENV_HOMEASSISTANT_REPOSITORY,
|
||||
ENV_SUPERVISOR_DEV,
|
||||
ENV_SUPERVISOR_MACHINE,
|
||||
ENV_SUPERVISOR_NAME,
|
||||
ENV_SUPERVISOR_SHARE,
|
||||
MACHINE_ID,
|
||||
SOCKET_DOCKER,
|
||||
SUPERVISOR_VERSION,
|
||||
CoreStates,
|
||||
LogLevel,
|
||||
UpdateChannels,
|
||||
)
|
||||
@ -34,6 +34,7 @@ from .hassos import HassOS
|
||||
from .homeassistant import HomeAssistant
|
||||
from .host import HostManager
|
||||
from .ingress import Ingress
|
||||
from .misc.filter import filter_data
|
||||
from .misc.hwmon import HwMonitor
|
||||
from .misc.scheduler import Scheduler
|
||||
from .misc.secrets import SecretsManager
|
||||
@ -169,7 +170,7 @@ def initialize_system_data(coresys: CoreSys) -> None:
|
||||
coresys.config.modify_log_level()
|
||||
|
||||
# Check if ENV is in development mode
|
||||
if bool(os.environ.get("SUPERVISOR_DEV", 0)):
|
||||
if bool(os.environ.get(ENV_SUPERVISOR_DEV, 0)):
|
||||
_LOGGER.warning("SUPERVISOR_DEV is set")
|
||||
coresys.updater.channel = UpdateChannels.DEV
|
||||
coresys.config.logging = LogLevel.DEBUG
|
||||
@ -280,54 +281,19 @@ def supervisor_debugger(coresys: CoreSys) -> None:
|
||||
|
||||
def setup_diagnostics(coresys: CoreSys) -> None:
|
||||
"""Sentry diagnostic backend."""
|
||||
_LOGGER.info("Initialize Supervisor Sentry")
|
||||
|
||||
def filter_data(event, hint):
|
||||
# Ignore issue if system is not supported or diagnostics is disabled
|
||||
if not coresys.config.diagnostics or not coresys.supported:
|
||||
return None
|
||||
|
||||
# Not full startup - missing information
|
||||
if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP):
|
||||
return event
|
||||
|
||||
# Update information
|
||||
event.setdefault("extra", {}).update(
|
||||
{
|
||||
"supervisor": {
|
||||
"machine": coresys.machine,
|
||||
"arch": coresys.arch.default,
|
||||
"docker": coresys.docker.info.version,
|
||||
"channel": coresys.updater.channel,
|
||||
"supervisor": coresys.supervisor.version,
|
||||
"os": coresys.hassos.version,
|
||||
"host": coresys.host.info.operating_system,
|
||||
"kernel": coresys.host.info.kernel,
|
||||
"core": coresys.homeassistant.version,
|
||||
"audio": coresys.plugins.audio.version,
|
||||
"dns": coresys.plugins.dns.version,
|
||||
"multicast": coresys.plugins.multicast.version,
|
||||
"cli": coresys.plugins.cli.version,
|
||||
}
|
||||
}
|
||||
)
|
||||
event.setdefault("tags", {}).update(
|
||||
{
|
||||
"installation_type": "os" if coresys.hassos.available else "supervised",
|
||||
"machine": coresys.machine,
|
||||
}
|
||||
)
|
||||
|
||||
return event
|
||||
def filter_event(event, hint):
|
||||
return filter_data(coresys, event, hint)
|
||||
|
||||
# Set log level
|
||||
sentry_logging = LoggingIntegration(
|
||||
level=logging.WARNING, event_level=logging.CRITICAL
|
||||
)
|
||||
|
||||
_LOGGER.info("Initialize Supervisor Sentry")
|
||||
sentry_sdk.init(
|
||||
dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612",
|
||||
before_send=filter_data,
|
||||
before_send=filter_event,
|
||||
max_breadcrumbs=30,
|
||||
integrations=[AioHttpIntegration(), sentry_logging],
|
||||
release=SUPERVISOR_VERSION,
|
||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
SUPERVISOR_VERSION = "232"
|
||||
SUPERVISOR_VERSION = "233"
|
||||
|
||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json"
|
||||
@ -36,6 +36,11 @@ SOCKET_DBUS = Path("/run/dbus/system_bus_socket")
|
||||
DOCKER_NETWORK = "hassio"
|
||||
DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23")
|
||||
DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24")
|
||||
DOCKER_IMAGE_DENYLIST = [
|
||||
"containrrr/watchtower",
|
||||
"pyouroboros/ouroboros",
|
||||
"v2tec/watchtower",
|
||||
]
|
||||
|
||||
DNS_SUFFIX = "local.hass.io"
|
||||
|
||||
@ -74,6 +79,7 @@ ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY"
|
||||
ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE"
|
||||
ENV_SUPERVISOR_NAME = "SUPERVISOR_NAME"
|
||||
ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE"
|
||||
ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV"
|
||||
|
||||
REQUEST_FROM = "HASSIO_FROM"
|
||||
|
||||
@ -163,6 +169,9 @@ ATTR_AUDIO_OUTPUT = "audio_output"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_OUTPUT = "output"
|
||||
ATTR_DISK = "disk"
|
||||
ATTR_DISK_FREE = "disk_free"
|
||||
ATTR_DISK_TOTAL = "disk_total"
|
||||
ATTR_DISK_USED = "disk_used"
|
||||
ATTR_SERIAL = "serial"
|
||||
ATTR_SECURITY = "security"
|
||||
ATTR_BUILD_FROM = "build_from"
|
||||
@ -328,6 +337,8 @@ ROLE_ALL = [ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_BACKUP, ROLE_MANAGER, ROLE_AD
|
||||
CHAN_ID = "chan_id"
|
||||
CHAN_TYPE = "chan_type"
|
||||
|
||||
SUPERVISED_SUPPORTED_OS = ["Debian GNU/Linux 10 (buster)"]
|
||||
|
||||
|
||||
class AddonStartup(str, Enum):
|
||||
"""Startup types of Add-on."""
|
||||
|
@ -5,7 +5,7 @@ import logging
|
||||
|
||||
import async_timeout
|
||||
|
||||
from .const import SOCKET_DBUS, AddonStartup, CoreStates
|
||||
from .const import SOCKET_DBUS, SUPERVISED_SUPPORTED_OS, AddonStartup, CoreStates
|
||||
from .coresys import CoreSys, CoreSysAttributes
|
||||
from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError
|
||||
|
||||
@ -19,12 +19,8 @@ class Core(CoreSysAttributes):
|
||||
"""Initialize Supervisor object."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self.state: CoreStates = CoreStates.INITIALIZE
|
||||
self._healthy: bool = True
|
||||
|
||||
@property
|
||||
def healthy(self) -> bool:
|
||||
"""Return True if system is healthy."""
|
||||
return self._healthy and self.sys_supported
|
||||
self.healthy: bool = True
|
||||
self.supported: bool = True
|
||||
|
||||
async def connect(self):
|
||||
"""Connect Supervisor container."""
|
||||
@ -32,21 +28,25 @@ class Core(CoreSysAttributes):
|
||||
|
||||
# If host docker is supported?
|
||||
if not self.sys_docker.info.supported_version:
|
||||
self.coresys.supported = False
|
||||
self.supported = False
|
||||
self.healthy = False
|
||||
_LOGGER.error(
|
||||
"Docker version %s is not supported by Supervisor!",
|
||||
self.sys_docker.info.version,
|
||||
)
|
||||
elif self.sys_docker.info.inside_lxc:
|
||||
self.coresys.supported = False
|
||||
self.supported = False
|
||||
self.healthy = False
|
||||
_LOGGER.error(
|
||||
"Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!"
|
||||
)
|
||||
self.sys_docker.info.check_requirements()
|
||||
|
||||
if self.sys_docker.info.check_requirements():
|
||||
self.supported = False
|
||||
|
||||
# Dbus available
|
||||
if not SOCKET_DBUS.exists():
|
||||
self.coresys.supported = False
|
||||
self.supported = False
|
||||
_LOGGER.error(
|
||||
"DBus is required for Home Assistant. This system is not supported!"
|
||||
)
|
||||
@ -58,13 +58,13 @@ class Core(CoreSysAttributes):
|
||||
self.sys_config.version == "dev"
|
||||
or self.sys_supervisor.instance.version == "dev"
|
||||
):
|
||||
self.coresys.supported = False
|
||||
self.supported = False
|
||||
_LOGGER.warning(
|
||||
"Found a development supervisor outside dev channel (%s)",
|
||||
self.sys_updater.channel,
|
||||
)
|
||||
elif self.sys_config.version != self.sys_supervisor.version:
|
||||
self._healthy = False
|
||||
self.healthy = False
|
||||
_LOGGER.error(
|
||||
"Update %s of Supervisor %s fails!",
|
||||
self.sys_config.version,
|
||||
@ -120,13 +120,42 @@ class Core(CoreSysAttributes):
|
||||
# Load secrets
|
||||
await self.sys_secrets.load()
|
||||
|
||||
# Check supported OS
|
||||
if not self.sys_hassos.available:
|
||||
if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS:
|
||||
self.supported = False
|
||||
_LOGGER.error(
|
||||
"Using '%s' as the OS is not supported",
|
||||
self.sys_host.info.operating_system,
|
||||
)
|
||||
else:
|
||||
# Check rauc connectivity on our OS
|
||||
if not self.sys_dbus.rauc.is_connected:
|
||||
self.healthy = False
|
||||
|
||||
# Check all DBUS connectivity
|
||||
if not self.sys_dbus.hostname.is_connected:
|
||||
self.supported = False
|
||||
_LOGGER.error("Hostname DBUS is not connected")
|
||||
if not self.sys_dbus.nmi_dns.is_connected:
|
||||
self.supported = False
|
||||
_LOGGER.error("NetworkManager DNS DBUS is not connected")
|
||||
if not self.sys_dbus.systemd.is_connected:
|
||||
self.supported = False
|
||||
_LOGGER.error("Systemd DBUS is not connected")
|
||||
|
||||
# Check if image names from denylist exist
|
||||
if await self.sys_run_in_executor(self.sys_docker.check_denylist_images):
|
||||
self.coresys.supported = False
|
||||
self.healthy = False
|
||||
|
||||
async def start(self):
|
||||
"""Start Supervisor orchestration."""
|
||||
self.state = CoreStates.STARTUP
|
||||
await self.sys_api.start()
|
||||
|
||||
# Check if system is healthy
|
||||
if not self.sys_supported:
|
||||
if not self.supported:
|
||||
_LOGGER.critical("System running in a unsupported environment!")
|
||||
elif not self.healthy:
|
||||
_LOGGER.critical(
|
||||
|
@ -48,9 +48,6 @@ class CoreSys:
|
||||
self._machine_id: Optional[str] = None
|
||||
self._machine: Optional[str] = None
|
||||
|
||||
# Static attributes
|
||||
self.supported: bool = True
|
||||
|
||||
# External objects
|
||||
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
||||
self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
|
||||
@ -462,11 +459,6 @@ class CoreSysAttributes:
|
||||
"""Return True if we run dev mode."""
|
||||
return self.coresys.dev
|
||||
|
||||
@property
|
||||
def sys_supported(self) -> bool:
|
||||
"""Return True if the system is supported."""
|
||||
return self.coresys.supported
|
||||
|
||||
@property
|
||||
def sys_timezone(self) -> str:
|
||||
"""Return timezone."""
|
||||
|
@ -9,7 +9,7 @@ import attr
|
||||
import docker
|
||||
from packaging import version as pkg_version
|
||||
|
||||
from ..const import DNS_SUFFIX, SOCKET_DOCKER
|
||||
from ..const import DNS_SUFFIX, DOCKER_IMAGE_DENYLIST, SOCKET_DOCKER
|
||||
from ..exceptions import DockerAPIError
|
||||
from .network import DockerNetwork
|
||||
|
||||
@ -60,6 +60,8 @@ class DockerInfo:
|
||||
if self.logging != "journald":
|
||||
_LOGGER.error("Docker logging driver %s is not supported!", self.logging)
|
||||
|
||||
return self.storage != "overlay2" or self.logging != "journald"
|
||||
|
||||
|
||||
class DockerAPI:
|
||||
"""Docker Supervisor wrapper.
|
||||
@ -230,3 +232,24 @@ class DockerAPI:
|
||||
_LOGGER.debug("Networks prune: %s", output)
|
||||
except docker.errors.APIError as err:
|
||||
_LOGGER.warning("Error for networks prune: %s", err)
|
||||
|
||||
def check_denylist_images(self) -> bool:
|
||||
"""Return a boolean if the host has images in the denylist."""
|
||||
denied_images = set()
|
||||
for image in self.images.list():
|
||||
for tag in image.tags:
|
||||
image_name = tag.split(":")[0]
|
||||
if (
|
||||
image_name in DOCKER_IMAGE_DENYLIST
|
||||
and image_name not in denied_images
|
||||
):
|
||||
denied_images.add(image_name)
|
||||
|
||||
if not denied_images:
|
||||
return False
|
||||
|
||||
_LOGGER.error(
|
||||
"Found images: '%s' which are not supported, remove these from the host!",
|
||||
", ".join(denied_images),
|
||||
)
|
||||
return True
|
||||
|
@ -31,7 +31,15 @@ class DockerNetwork:
|
||||
@property
|
||||
def containers(self) -> List[docker.models.containers.Container]:
|
||||
"""Return of connected containers from network."""
|
||||
return self.network.containers
|
||||
containers: List[docker.models.containers.Container] = []
|
||||
for cid, data in self.network.attrs.get("Containers", {}).items():
|
||||
try:
|
||||
containers.append(self.docker.containers.get(cid))
|
||||
except docker.errors.APIError as err:
|
||||
_LOGGER.warning("Docker network is corrupt! %s - run autofix", err)
|
||||
self.stale_cleanup(data.get("Name", cid))
|
||||
|
||||
return containers
|
||||
|
||||
@property
|
||||
def gateway(self) -> IPv4Address:
|
||||
|
@ -105,6 +105,10 @@ class AddonsError(HassioError):
|
||||
"""Addons exception."""
|
||||
|
||||
|
||||
class AddonConfigurationError(AddonsError):
|
||||
"""Error with add-on configuration."""
|
||||
|
||||
|
||||
class AddonsNotSupportedError(HassioNotSupportedError):
|
||||
"""Addons don't support a function."""
|
||||
|
||||
|
@ -66,7 +66,7 @@ class HostManager(CoreSysAttributes):
|
||||
return self._sound
|
||||
|
||||
@property
|
||||
def supperted_features(self):
|
||||
def supported_features(self):
|
||||
"""Return a list of supported host features."""
|
||||
features = []
|
||||
|
||||
|
@ -51,6 +51,20 @@ class InfoCenter(CoreSysAttributes):
|
||||
"""Return local CPE."""
|
||||
return self.sys_dbus.hostname.cpe
|
||||
|
||||
@property
|
||||
def total_space(self) -> float:
|
||||
"""Return total space (GiB) on disk for supervisor data directory."""
|
||||
return self.coresys.hardware.get_disk_total_space(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def used_space(self) -> float:
|
||||
"""Return used space (GiB) on disk for supervisor data directory."""
|
||||
return self.coresys.hardware.get_disk_used_space(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def free_space(self) -> float:
|
||||
"""Return available space (GiB) on disk for supervisor data directory."""
|
||||
|
@ -28,8 +28,8 @@ class Ingress(JsonConfig, CoreSysAttributes):
|
||||
def get(self, token: str) -> Optional[Addon]:
|
||||
"""Return addon they have this ingress token."""
|
||||
if token not in self.tokens:
|
||||
self._update_token_list()
|
||||
return self.sys_addons.get(self.tokens.get(token))
|
||||
return None
|
||||
return self.sys_addons.get(self.tokens[token], local_only=True)
|
||||
|
||||
@property
|
||||
def sessions(self) -> Dict[str, float]:
|
||||
@ -61,6 +61,7 @@ class Ingress(JsonConfig, CoreSysAttributes):
|
||||
async def reload(self) -> None:
|
||||
"""Reload/Validate sessions."""
|
||||
self._cleanup_sessions()
|
||||
self._update_token_list()
|
||||
|
||||
async def unload(self) -> None:
|
||||
"""Shutdown sessions."""
|
||||
|
89
supervisor/misc/filter.py
Normal file
89
supervisor/misc/filter.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""Filter tools."""
|
||||
import os
|
||||
import re
|
||||
|
||||
from aiohttp import hdrs
|
||||
|
||||
from ..const import ENV_SUPERVISOR_DEV, HEADER_TOKEN_OLD, CoreStates
|
||||
from ..coresys import CoreSys
|
||||
from ..exceptions import AddonConfigurationError
|
||||
|
||||
RE_URL: re.Pattern = re.compile(r"(\w+:\/\/)(.*\.\w+)(.*)")
|
||||
|
||||
|
||||
def sanitize_url(url: str) -> str:
|
||||
"""Return a sanitized url."""
|
||||
if not re.match(RE_URL, url):
|
||||
# Not a URL, just return it back
|
||||
return url
|
||||
|
||||
return re.sub(RE_URL, r"\1example.com\3", url)
|
||||
|
||||
|
||||
def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
"""Filter event data before sending to sentry."""
|
||||
dev_env: bool = bool(os.environ.get(ENV_SUPERVISOR_DEV, 0))
|
||||
|
||||
# Ignore some exceptions
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
if isinstance(exc_value, (AddonConfigurationError)):
|
||||
return None
|
||||
|
||||
# Ignore issue if system is not supported or diagnostics is disabled
|
||||
if not coresys.config.diagnostics or not coresys.supported or dev_env:
|
||||
return None
|
||||
|
||||
# Not full startup - missing information
|
||||
if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP):
|
||||
return event
|
||||
|
||||
# Update information
|
||||
event.setdefault("extra", {}).update(
|
||||
{
|
||||
"supervisor": {
|
||||
"machine": coresys.machine,
|
||||
"arch": coresys.arch.default,
|
||||
"docker": coresys.docker.info.version,
|
||||
"channel": coresys.updater.channel,
|
||||
"supervisor": coresys.supervisor.version,
|
||||
"os": coresys.hassos.version,
|
||||
"host": coresys.host.info.operating_system,
|
||||
"kernel": coresys.host.info.kernel,
|
||||
"core": coresys.homeassistant.version,
|
||||
"audio": coresys.plugins.audio.version,
|
||||
"dns": coresys.plugins.dns.version,
|
||||
"multicast": coresys.plugins.multicast.version,
|
||||
"cli": coresys.plugins.cli.version,
|
||||
"disk_free_space": coresys.host.info.free_space,
|
||||
}
|
||||
}
|
||||
)
|
||||
event.setdefault("tags", []).extend(
|
||||
[
|
||||
["installation_type", "os" if coresys.hassos.available else "supervised"],
|
||||
["machine", coresys.machine],
|
||||
],
|
||||
)
|
||||
|
||||
# Sanitize event
|
||||
for i, tag in enumerate(event.get("tags", [])):
|
||||
key, value = tag
|
||||
if key == "url":
|
||||
event["tags"][i] = [key, sanitize_url(value)]
|
||||
|
||||
if event.get("request"):
|
||||
if event["request"].get("url"):
|
||||
event["request"]["url"] = sanitize_url(event["request"]["url"])
|
||||
|
||||
for i, header in enumerate(event["request"].get("headers", [])):
|
||||
key, value = header
|
||||
if key == hdrs.REFERER:
|
||||
event["request"]["headers"][i] = [key, sanitize_url(value)]
|
||||
|
||||
if key == HEADER_TOKEN_OLD:
|
||||
event["request"]["headers"][i] = [key, "XXXXXXXXXXXXXXXXXXX"]
|
||||
|
||||
if key in [hdrs.HOST, hdrs.X_FORWARDED_HOST]:
|
||||
event["request"]["headers"][i] = [key, "example.com"]
|
||||
return event
|
@ -196,6 +196,16 @@ class Hardware:
|
||||
|
||||
return datetime.utcfromtimestamp(int(found.group(1)))
|
||||
|
||||
def get_disk_total_space(self, path: Union[str, Path]) -> float:
|
||||
"""Return total space (GiB) on disk for path."""
|
||||
total, _, _ = shutil.disk_usage(path)
|
||||
return round(total / (1024.0 ** 3), 1)
|
||||
|
||||
def get_disk_used_space(self, path: Union[str, Path]) -> float:
|
||||
"""Return used space (GiB) on disk for path."""
|
||||
_, used, _ = shutil.disk_usage(path)
|
||||
return round(used / (1024.0 ** 3), 1)
|
||||
|
||||
def get_disk_free_space(self, path: Union[str, Path]) -> float:
|
||||
"""Return free space (GiB) on disk for path."""
|
||||
_, _, free = shutil.disk_usage(path)
|
||||
|
@ -28,6 +28,7 @@ class HwMonitor(CoreSysAttributes):
|
||||
self.monitor = pyudev.Monitor.from_netlink(self.context)
|
||||
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
|
||||
except OSError:
|
||||
self.sys_core.healthy = False
|
||||
_LOGGER.critical("Not privileged to run udev monitor!")
|
||||
else:
|
||||
self.observer.start()
|
||||
|
@ -8,7 +8,8 @@ import socket
|
||||
from typing import Optional
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
|
||||
|
||||
RE_STRING: re.Pattern = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
|
||||
|
||||
|
||||
def convert_to_ascii(raw: bytes) -> str:
|
||||
|
@ -5,7 +5,7 @@ import pytest
|
||||
|
||||
from supervisor.bootstrap import initialize_coresys
|
||||
|
||||
# pylint: disable=redefined-outer-name
|
||||
# pylint: disable=redefined-outer-name, protected-access
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -26,6 +26,7 @@ async def coresys(loop, docker):
|
||||
coresys_obj = await initialize_coresys()
|
||||
|
||||
coresys_obj.ingress.save_data = MagicMock()
|
||||
coresys_obj.arch._default_arch = "amd64"
|
||||
|
||||
yield coresys_obj
|
||||
|
||||
|
94
tests/misc/test_filter_data.py
Normal file
94
tests/misc/test_filter_data.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Test sentry data filter."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreStates
|
||||
from supervisor.exceptions import AddonConfigurationError
|
||||
from supervisor.misc.filter import filter_data
|
||||
|
||||
SAMPLE_EVENT = {"sample": "event"}
|
||||
|
||||
|
||||
def test_ignored_exception(coresys):
|
||||
"""Test ignored exceptions."""
|
||||
hint = {"exc_info": (None, AddonConfigurationError(), None)}
|
||||
assert filter_data(coresys, SAMPLE_EVENT, hint) is None
|
||||
|
||||
|
||||
def test_diagnostics_disabled(coresys):
|
||||
"""Test if diagnostics is disabled."""
|
||||
coresys.config.diagnostics = False
|
||||
coresys.supported = True
|
||||
assert filter_data(coresys, SAMPLE_EVENT, {}) is None
|
||||
|
||||
|
||||
def test_not_supported(coresys):
|
||||
"""Test if not supported."""
|
||||
coresys.config.diagnostics = True
|
||||
coresys.supported = False
|
||||
assert filter_data(coresys, SAMPLE_EVENT, {}) is None
|
||||
|
||||
|
||||
def test_is_dev(coresys):
|
||||
"""Test if dev."""
|
||||
coresys.config.diagnostics = True
|
||||
coresys.supported = True
|
||||
with patch("os.environ", return_value=[("ENV_SUPERVISOR_DEV", "1")]):
|
||||
assert filter_data(coresys, SAMPLE_EVENT, {}) is None
|
||||
|
||||
|
||||
def test_not_started(coresys):
|
||||
"""Test if supervisor not fully started."""
|
||||
coresys.config.diagnostics = True
|
||||
coresys.supported = True
|
||||
|
||||
coresys.core.state = CoreStates.INITIALIZE
|
||||
assert filter_data(coresys, SAMPLE_EVENT, {}) == SAMPLE_EVENT
|
||||
|
||||
coresys.core.state = CoreStates.SETUP
|
||||
assert filter_data(coresys, SAMPLE_EVENT, {}) == SAMPLE_EVENT
|
||||
|
||||
|
||||
def test_defaults(coresys):
|
||||
"""Test event defaults."""
|
||||
coresys.config.diagnostics = True
|
||||
coresys.supported = True
|
||||
|
||||
coresys.core.state = CoreStates.RUNNING
|
||||
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))):
|
||||
filtered = filter_data(coresys, SAMPLE_EVENT, {})
|
||||
|
||||
assert ["installation_type", "supervised"] in filtered["tags"]
|
||||
assert filtered["extra"]["supervisor"]["arch"] == "amd64"
|
||||
|
||||
|
||||
def test_sanitize(coresys):
|
||||
"""Test event sanitation."""
|
||||
event = {
|
||||
"tags": [["url", "https://mydomain.com"]],
|
||||
"request": {
|
||||
"url": "https://mydomain.com",
|
||||
"headers": [
|
||||
["Host", "mydomain.com"],
|
||||
["Referer", "https://mydomain.com/api/hassio_ingress/xxx-xxx/"],
|
||||
["X-Forwarded-Host", "mydomain.com"],
|
||||
["X-Hassio-Key", "xxx"],
|
||||
],
|
||||
},
|
||||
}
|
||||
coresys.config.diagnostics = True
|
||||
coresys.supported = True
|
||||
|
||||
coresys.core.state = CoreStates.RUNNING
|
||||
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))):
|
||||
filtered = filter_data(coresys, event, {})
|
||||
|
||||
assert ["url", "https://example.com"] in filtered["tags"]
|
||||
|
||||
assert filtered["request"]["url"] == "https://example.com"
|
||||
|
||||
assert ["Host", "example.com"] in filtered["request"]["headers"]
|
||||
assert ["Referer", "https://example.com/api/hassio_ingress/xxx-xxx/"] in filtered[
|
||||
"request"
|
||||
]["headers"]
|
||||
assert ["X-Forwarded-Host", "example.com"] in filtered["request"]["headers"]
|
||||
assert ["X-Hassio-Key", "XXXXXXXXXXXXXXXXXXX"] in filtered["request"]["headers"]
|
@ -41,3 +41,21 @@ def test_free_space():
|
||||
free = system.get_disk_free_space("/data")
|
||||
|
||||
assert free == 2.0
|
||||
|
||||
|
||||
def test_total_space():
|
||||
"""Test total space helper."""
|
||||
system = Hardware()
|
||||
with patch("shutil.disk_usage", return_value=(10 * (1024.0 ** 3), 42, 42)):
|
||||
total = system.get_disk_total_space("/data")
|
||||
|
||||
assert total == 10.0
|
||||
|
||||
|
||||
def test_used_space():
|
||||
"""Test used space helper."""
|
||||
system = Hardware()
|
||||
with patch("shutil.disk_usage", return_value=(42, 8 * (1024.0 ** 3), 42)):
|
||||
used = system.get_disk_used_space("/data")
|
||||
|
||||
assert used == 8.0
|
||||
|
9
tests/misc/test_sanitise_url.py
Normal file
9
tests/misc/test_sanitise_url.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""Test supervisor.utils.sanitize_url."""
|
||||
from supervisor.misc.filter import sanitize_url
|
||||
|
||||
|
||||
def test_sanitize_url():
|
||||
"""Test supervisor.utils.sanitize_url."""
|
||||
assert sanitize_url("test") == "test"
|
||||
assert sanitize_url("http://my.duckdns.org") == "http://example.com"
|
||||
assert sanitize_url("http://my.duckdns.org/test") == "http://example.com/test"
|
Loading…
x
Reference in New Issue
Block a user