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.
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"
}
```

View File

@ -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

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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."""

View File

@ -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(

View File

@ -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."""

View File

@ -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

View File

@ -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:

View File

@ -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."""

View File

@ -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 = []

View File

@ -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."""

View File

@ -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
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)))
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)

View File

@ -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()

View File

@ -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:

View File

@ -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

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