Merge pull request #1913 from home-assistant/dev

Release 233
This commit is contained in:
Pascal Vizeli 2020-08-14 14:12:11 +02:00 committed by GitHub
commit eba1d01fc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 379 additions and 88 deletions

15
API.md
View File

@ -25,7 +25,7 @@ On success / Code 200:
To access the API you need use an authorization header with a `Bearer` token. 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_TOKEN` environment variable.
### Supervisor ### Supervisor
@ -242,13 +242,16 @@ return:
```json ```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", "chassis": "specific|null",
"cpe": "xy|null",
"deployment": "stable|beta|dev|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"
} }
``` ```

View File

@ -14,6 +14,6 @@ pulsectl==20.5.1
pytz==2020.1 pytz==2020.1
pyudev==0.22.0 pyudev==0.22.0
ruamel.yaml==0.15.100 ruamel.yaml==0.15.100
sentry-sdk==0.16.3 sentry-sdk==0.16.4
uvloop==0.14.0 uvloop==0.14.0
voluptuous==0.11.7 voluptuous==0.11.7

View File

@ -45,7 +45,7 @@ class AddonManager(CoreSysAttributes):
"""Return a list of all installed add-ons.""" """Return a list of all installed add-ons."""
return list(self.local.values()) 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. """Return an add-on from slug.
Prio: Prio:
@ -54,7 +54,9 @@ class AddonManager(CoreSysAttributes):
""" """
if addon_slug in self.local: if addon_slug in self.local:
return self.local[addon_slug] 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]: def from_token(self, token: str) -> Optional[Addon]:
"""Return an add-on from Supervisor token.""" """Return an add-on from Supervisor token."""
@ -156,7 +158,12 @@ class AddonManager(CoreSysAttributes):
raise AddonsError() from None raise AddonsError() from None
else: else:
self.local[slug] = addon 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: async def uninstall(self, slug: str) -> None:
"""Remove an add-on.""" """Remove an add-on."""
@ -188,7 +195,9 @@ class AddonManager(CoreSysAttributes):
await self.sys_ingress.update_hass_panel(addon) await self.sys_ingress.update_hass_panel(addon)
# Cleanup Ingress dynamic port assignment # 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 # Cleanup discovery data
for message in self.sys_discovery.list_messages: for message in self.sys_discovery.list_messages:
@ -302,6 +311,7 @@ class AddonManager(CoreSysAttributes):
# Update ingress # Update ingress
if addon.with_ingress: if addon.with_ingress:
await self.sys_ingress.reload()
with suppress(HomeAssistantAPIError): with suppress(HomeAssistantAPIError):
await self.sys_ingress.update_hass_panel(addon) await self.sys_ingress.update_hass_panel(addon)

View File

@ -43,6 +43,7 @@ from ..coresys import CoreSys
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import ( from ..exceptions import (
AddonConfigurationError,
AddonsError, AddonsError,
AddonsNotSupportedError, AddonsNotSupportedError,
DockerAPIError, DockerAPIError,
@ -375,7 +376,7 @@ class Addon(AddonModel):
_LOGGER.debug("Add-on %s write options: %s", self.slug, options) _LOGGER.debug("Add-on %s write options: %s", self.slug, options)
return return
raise AddonsError() raise AddonConfigurationError()
async def remove_data(self) -> None: async def remove_data(self) -> None:
"""Remove add-on data.""" """Remove add-on data."""

View File

@ -11,6 +11,9 @@ from ..const import (
ATTR_CPE, ATTR_CPE,
ATTR_DEPLOYMENT, ATTR_DEPLOYMENT,
ATTR_DESCRIPTON, ATTR_DESCRIPTON,
ATTR_DISK_FREE,
ATTR_DISK_TOTAL,
ATTR_DISK_USED,
ATTR_FEATURES, ATTR_FEATURES,
ATTR_HOSTNAME, ATTR_HOSTNAME,
ATTR_KERNEL, ATTR_KERNEL,
@ -39,11 +42,14 @@ class APIHost(CoreSysAttributes):
return { return {
ATTR_CHASSIS: self.sys_host.info.chassis, ATTR_CHASSIS: self.sys_host.info.chassis,
ATTR_CPE: self.sys_host.info.cpe, 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_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_KERNEL: self.sys_host.info.kernel,
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
} }
@api_process @api_process

View File

@ -39,7 +39,7 @@ class APIInfo(CoreSysAttributes):
ATTR_MACHINE: self.sys_machine, ATTR_MACHINE: self.sys_machine,
ATTR_ARCH: self.sys_arch.default, ATTR_ARCH: self.sys_arch.default,
ATTR_SUPPORTED_ARCH: self.sys_arch.supported, 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_CHANNEL: self.sys_updater.channel,
ATTR_LOGGING: self.sys_config.logging, ATTR_LOGGING: self.sys_config.logging,
ATTR_TIMEZONE: self.sys_timezone, ATTR_TIMEZONE: self.sys_timezone,

View File

@ -100,7 +100,7 @@ class APISupervisor(CoreSysAttributes):
ATTR_VERSION_LATEST: self.sys_updater.version_supervisor, ATTR_VERSION_LATEST: self.sys_updater.version_supervisor,
ATTR_CHANNEL: self.sys_updater.channel, ATTR_CHANNEL: self.sys_updater.channel,
ATTR_ARCH: self.sys_supervisor.arch, ATTR_ARCH: self.sys_supervisor.arch,
ATTR_SUPPORTED: self.sys_supported, ATTR_SUPPORTED: self.sys_core.supported,
ATTR_HEALTHY: self.sys_core.healthy, ATTR_HEALTHY: self.sys_core.healthy,
ATTR_IP_ADDRESS: str(self.sys_supervisor.ip_address), ATTR_IP_ADDRESS: str(self.sys_supervisor.ip_address),
ATTR_WAIT_BOOT: self.sys_config.wait_boot, ATTR_WAIT_BOOT: self.sys_config.wait_boot,

View File

@ -16,13 +16,13 @@ from .arch import CpuArch
from .auth import Auth from .auth import Auth
from .const import ( from .const import (
ENV_HOMEASSISTANT_REPOSITORY, ENV_HOMEASSISTANT_REPOSITORY,
ENV_SUPERVISOR_DEV,
ENV_SUPERVISOR_MACHINE, ENV_SUPERVISOR_MACHINE,
ENV_SUPERVISOR_NAME, ENV_SUPERVISOR_NAME,
ENV_SUPERVISOR_SHARE, ENV_SUPERVISOR_SHARE,
MACHINE_ID, MACHINE_ID,
SOCKET_DOCKER, SOCKET_DOCKER,
SUPERVISOR_VERSION, SUPERVISOR_VERSION,
CoreStates,
LogLevel, LogLevel,
UpdateChannels, UpdateChannels,
) )
@ -34,6 +34,7 @@ from .hassos import HassOS
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .host import HostManager from .host import HostManager
from .ingress import Ingress from .ingress import Ingress
from .misc.filter import filter_data
from .misc.hwmon import HwMonitor from .misc.hwmon import HwMonitor
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
from .misc.secrets import SecretsManager from .misc.secrets import SecretsManager
@ -169,7 +170,7 @@ def initialize_system_data(coresys: CoreSys) -> None:
coresys.config.modify_log_level() coresys.config.modify_log_level()
# Check if ENV is in development mode # 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") _LOGGER.warning("SUPERVISOR_DEV is set")
coresys.updater.channel = UpdateChannels.DEV coresys.updater.channel = UpdateChannels.DEV
coresys.config.logging = LogLevel.DEBUG coresys.config.logging = LogLevel.DEBUG
@ -280,54 +281,19 @@ def supervisor_debugger(coresys: CoreSys) -> None:
def setup_diagnostics(coresys: CoreSys) -> None: def setup_diagnostics(coresys: CoreSys) -> None:
"""Sentry diagnostic backend.""" """Sentry diagnostic backend."""
_LOGGER.info("Initialize Supervisor Sentry")
def filter_data(event, hint): def filter_event(event, hint):
# Ignore issue if system is not supported or diagnostics is disabled return filter_data(coresys, event, hint)
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
# Set log level # Set log level
sentry_logging = LoggingIntegration( sentry_logging = LoggingIntegration(
level=logging.WARNING, event_level=logging.CRITICAL level=logging.WARNING, event_level=logging.CRITICAL
) )
_LOGGER.info("Initialize Supervisor Sentry")
sentry_sdk.init( sentry_sdk.init(
dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612", dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612",
before_send=filter_data, before_send=filter_event,
max_breadcrumbs=30, max_breadcrumbs=30,
integrations=[AioHttpIntegration(), sentry_logging], integrations=[AioHttpIntegration(), sentry_logging],
release=SUPERVISOR_VERSION, release=SUPERVISOR_VERSION,

View File

@ -3,7 +3,7 @@ from enum import Enum
from ipaddress import ip_network from ipaddress import ip_network
from pathlib import Path from pathlib import Path
SUPERVISOR_VERSION = "232" SUPERVISOR_VERSION = "233"
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json" 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 = "hassio"
DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23") DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23")
DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24") DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24")
DOCKER_IMAGE_DENYLIST = [
"containrrr/watchtower",
"pyouroboros/ouroboros",
"v2tec/watchtower",
]
DNS_SUFFIX = "local.hass.io" DNS_SUFFIX = "local.hass.io"
@ -74,6 +79,7 @@ ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY"
ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE" ENV_SUPERVISOR_SHARE = "SUPERVISOR_SHARE"
ENV_SUPERVISOR_NAME = "SUPERVISOR_NAME" ENV_SUPERVISOR_NAME = "SUPERVISOR_NAME"
ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE" ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE"
ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV"
REQUEST_FROM = "HASSIO_FROM" REQUEST_FROM = "HASSIO_FROM"
@ -163,6 +169,9 @@ ATTR_AUDIO_OUTPUT = "audio_output"
ATTR_INPUT = "input" ATTR_INPUT = "input"
ATTR_OUTPUT = "output" ATTR_OUTPUT = "output"
ATTR_DISK = "disk" ATTR_DISK = "disk"
ATTR_DISK_FREE = "disk_free"
ATTR_DISK_TOTAL = "disk_total"
ATTR_DISK_USED = "disk_used"
ATTR_SERIAL = "serial" ATTR_SERIAL = "serial"
ATTR_SECURITY = "security" ATTR_SECURITY = "security"
ATTR_BUILD_FROM = "build_from" 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_ID = "chan_id"
CHAN_TYPE = "chan_type" CHAN_TYPE = "chan_type"
SUPERVISED_SUPPORTED_OS = ["Debian GNU/Linux 10 (buster)"]
class AddonStartup(str, Enum): class AddonStartup(str, Enum):
"""Startup types of Add-on.""" """Startup types of Add-on."""

View File

@ -5,7 +5,7 @@ import logging
import async_timeout 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 .coresys import CoreSys, CoreSysAttributes
from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError
@ -19,12 +19,8 @@ class Core(CoreSysAttributes):
"""Initialize Supervisor object.""" """Initialize Supervisor object."""
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.state: CoreStates = CoreStates.INITIALIZE self.state: CoreStates = CoreStates.INITIALIZE
self._healthy: bool = True self.healthy: bool = True
self.supported: bool = True
@property
def healthy(self) -> bool:
"""Return True if system is healthy."""
return self._healthy and self.sys_supported
async def connect(self): async def connect(self):
"""Connect Supervisor container.""" """Connect Supervisor container."""
@ -32,21 +28,25 @@ class Core(CoreSysAttributes):
# If host docker is supported? # If host docker is supported?
if not self.sys_docker.info.supported_version: if not self.sys_docker.info.supported_version:
self.coresys.supported = False self.supported = False
self.healthy = False
_LOGGER.error( _LOGGER.error(
"Docker version %s is not supported by Supervisor!", "Docker version %s is not supported by Supervisor!",
self.sys_docker.info.version, self.sys_docker.info.version,
) )
elif self.sys_docker.info.inside_lxc: elif self.sys_docker.info.inside_lxc:
self.coresys.supported = False self.supported = False
self.healthy = False
_LOGGER.error( _LOGGER.error(
"Detected Docker running inside LXC. Running Home Assistant with the Supervisor on LXC is not supported!" "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 # Dbus available
if not SOCKET_DBUS.exists(): if not SOCKET_DBUS.exists():
self.coresys.supported = False self.supported = False
_LOGGER.error( _LOGGER.error(
"DBus is required for Home Assistant. This system is not supported!" "DBus is required for Home Assistant. This system is not supported!"
) )
@ -58,13 +58,13 @@ class Core(CoreSysAttributes):
self.sys_config.version == "dev" self.sys_config.version == "dev"
or self.sys_supervisor.instance.version == "dev" or self.sys_supervisor.instance.version == "dev"
): ):
self.coresys.supported = False self.supported = False
_LOGGER.warning( _LOGGER.warning(
"Found a development supervisor outside dev channel (%s)", "Found a development supervisor outside dev channel (%s)",
self.sys_updater.channel, self.sys_updater.channel,
) )
elif self.sys_config.version != self.sys_supervisor.version: elif self.sys_config.version != self.sys_supervisor.version:
self._healthy = False self.healthy = False
_LOGGER.error( _LOGGER.error(
"Update %s of Supervisor %s fails!", "Update %s of Supervisor %s fails!",
self.sys_config.version, self.sys_config.version,
@ -120,13 +120,42 @@ class Core(CoreSysAttributes):
# Load secrets # Load secrets
await self.sys_secrets.load() 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): async def start(self):
"""Start Supervisor orchestration.""" """Start Supervisor orchestration."""
self.state = CoreStates.STARTUP self.state = CoreStates.STARTUP
await self.sys_api.start() await self.sys_api.start()
# Check if system is healthy # Check if system is healthy
if not self.sys_supported: if not self.supported:
_LOGGER.critical("System running in a unsupported environment!") _LOGGER.critical("System running in a unsupported environment!")
elif not self.healthy: elif not self.healthy:
_LOGGER.critical( _LOGGER.critical(

View File

@ -48,9 +48,6 @@ class CoreSys:
self._machine_id: Optional[str] = None self._machine_id: Optional[str] = None
self._machine: Optional[str] = None self._machine: Optional[str] = None
# Static attributes
self.supported: bool = True
# External objects # External objects
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop() self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
self._websession: aiohttp.ClientSession = aiohttp.ClientSession() self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
@ -462,11 +459,6 @@ class CoreSysAttributes:
"""Return True if we run dev mode.""" """Return True if we run dev mode."""
return self.coresys.dev return self.coresys.dev
@property
def sys_supported(self) -> bool:
"""Return True if the system is supported."""
return self.coresys.supported
@property @property
def sys_timezone(self) -> str: def sys_timezone(self) -> str:
"""Return timezone.""" """Return timezone."""

View File

@ -9,7 +9,7 @@ import attr
import docker import docker
from packaging import version as pkg_version 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 ..exceptions import DockerAPIError
from .network import DockerNetwork from .network import DockerNetwork
@ -60,6 +60,8 @@ class DockerInfo:
if self.logging != "journald": if self.logging != "journald":
_LOGGER.error("Docker logging driver %s is not supported!", self.logging) _LOGGER.error("Docker logging driver %s is not supported!", self.logging)
return self.storage != "overlay2" or self.logging != "journald"
class DockerAPI: class DockerAPI:
"""Docker Supervisor wrapper. """Docker Supervisor wrapper.
@ -230,3 +232,24 @@ class DockerAPI:
_LOGGER.debug("Networks prune: %s", output) _LOGGER.debug("Networks prune: %s", output)
except docker.errors.APIError as err: except docker.errors.APIError as err:
_LOGGER.warning("Error for networks prune: %s", 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

View File

@ -31,7 +31,15 @@ class DockerNetwork:
@property @property
def containers(self) -> List[docker.models.containers.Container]: def containers(self) -> List[docker.models.containers.Container]:
"""Return of connected containers from network.""" """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 @property
def gateway(self) -> IPv4Address: def gateway(self) -> IPv4Address:

View File

@ -105,6 +105,10 @@ class AddonsError(HassioError):
"""Addons exception.""" """Addons exception."""
class AddonConfigurationError(AddonsError):
"""Error with add-on configuration."""
class AddonsNotSupportedError(HassioNotSupportedError): class AddonsNotSupportedError(HassioNotSupportedError):
"""Addons don't support a function.""" """Addons don't support a function."""

View File

@ -66,7 +66,7 @@ class HostManager(CoreSysAttributes):
return self._sound return self._sound
@property @property
def supperted_features(self): def supported_features(self):
"""Return a list of supported host features.""" """Return a list of supported host features."""
features = [] features = []

View File

@ -51,6 +51,20 @@ class InfoCenter(CoreSysAttributes):
"""Return local CPE.""" """Return local CPE."""
return self.sys_dbus.hostname.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 @property
def free_space(self) -> float: def free_space(self) -> float:
"""Return available space (GiB) on disk for supervisor data directory.""" """Return available space (GiB) on disk for supervisor data directory."""

View File

@ -28,8 +28,8 @@ class Ingress(JsonConfig, CoreSysAttributes):
def get(self, token: str) -> Optional[Addon]: def get(self, token: str) -> Optional[Addon]:
"""Return addon they have this ingress token.""" """Return addon they have this ingress token."""
if token not in self.tokens: if token not in self.tokens:
self._update_token_list() return None
return self.sys_addons.get(self.tokens.get(token)) return self.sys_addons.get(self.tokens[token], local_only=True)
@property @property
def sessions(self) -> Dict[str, float]: def sessions(self) -> Dict[str, float]:
@ -61,6 +61,7 @@ class Ingress(JsonConfig, CoreSysAttributes):
async def reload(self) -> None: async def reload(self) -> None:
"""Reload/Validate sessions.""" """Reload/Validate sessions."""
self._cleanup_sessions() self._cleanup_sessions()
self._update_token_list()
async def unload(self) -> None: async def unload(self) -> None:
"""Shutdown sessions.""" """Shutdown sessions."""

89
supervisor/misc/filter.py Normal file
View 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

View File

@ -196,6 +196,16 @@ class Hardware:
return datetime.utcfromtimestamp(int(found.group(1))) 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: def get_disk_free_space(self, path: Union[str, Path]) -> float:
"""Return free space (GiB) on disk for path.""" """Return free space (GiB) on disk for path."""
_, _, free = shutil.disk_usage(path) _, _, free = shutil.disk_usage(path)

View File

@ -28,6 +28,7 @@ class HwMonitor(CoreSysAttributes):
self.monitor = pyudev.Monitor.from_netlink(self.context) self.monitor = pyudev.Monitor.from_netlink(self.context)
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events) self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
except OSError: except OSError:
self.sys_core.healthy = False
_LOGGER.critical("Not privileged to run udev monitor!") _LOGGER.critical("Not privileged to run udev monitor!")
else: else:
self.observer.start() self.observer.start()

View File

@ -8,7 +8,8 @@ import socket
from typing import Optional from typing import Optional
_LOGGER: logging.Logger = logging.getLogger(__name__) _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: def convert_to_ascii(raw: bytes) -> str:

View File

@ -5,7 +5,7 @@ import pytest
from supervisor.bootstrap import initialize_coresys from supervisor.bootstrap import initialize_coresys
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name, protected-access
@pytest.fixture @pytest.fixture
@ -26,6 +26,7 @@ async def coresys(loop, docker):
coresys_obj = await initialize_coresys() coresys_obj = await initialize_coresys()
coresys_obj.ingress.save_data = MagicMock() coresys_obj.ingress.save_data = MagicMock()
coresys_obj.arch._default_arch = "amd64"
yield coresys_obj yield coresys_obj

View 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"]

View File

@ -41,3 +41,21 @@ def test_free_space():
free = system.get_disk_free_space("/data") free = system.get_disk_free_space("/data")
assert free == 2.0 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

View 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"