Merge pull request #1944 from home-assistant/dev

Release 235
This commit is contained in:
Pascal Vizeli 2020-08-18 15:20:40 +02:00 committed by GitHub
commit e9802f92c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 317 additions and 171 deletions

View File

@ -429,4 +429,4 @@ jobs:
coverage report coverage report
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v1.0.12 uses: codecov/codecov-action@v1.0.13

21
.github/workflows/sentry.yaml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Sentry Release
# yamllint disable-line rule:truthy
on:
release:
types: [published, prereleased]
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v2
- name: Sentry Release
uses: getsentry/action-release@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: production

1
API.md
View File

@ -535,6 +535,7 @@ Get all available add-ons.
"stdin": "bool", "stdin": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx", "webui": "null|http(s)://[HOST]:port/xy/zx",
"gpio": "bool", "gpio": "bool",
"usb": "[physical_path_to_usb_device]",
"kernel_modules": "bool", "kernel_modules": "bool",
"devicetree": "bool", "devicetree": "bool",
"docker_api": "bool", "docker_api": "bool",

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.4 sentry-sdk==0.16.5
uvloop==0.14.0 uvloop==0.14.0
voluptuous==0.11.7 voluptuous==0.11.7

View File

@ -7,7 +7,7 @@ pre-commit==2.6.0
pydocstyle==5.0.2 pydocstyle==5.0.2
pylint==2.5.3 pylint==2.5.3
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
pytest-cov==2.10.0 pytest-cov==2.10.1
pytest-timeout==1.4.2 pytest-timeout==1.4.2
pytest==6.0.1 pytest==6.0.1
pyupgrade==2.7.2 pyupgrade==2.7.2

View File

@ -58,6 +58,7 @@ from ..const import (
ATTR_TMPFS, ATTR_TMPFS,
ATTR_UDEV, ATTR_UDEV,
ATTR_URL, ATTR_URL,
ATTR_USB,
ATTR_VERSION, ATTR_VERSION,
ATTR_VIDEO, ATTR_VIDEO,
ATTR_WEBUI, ATTR_WEBUI,
@ -292,11 +293,6 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return devices of add-on.""" """Return devices of add-on."""
return self.data.get(ATTR_DEVICES, []) return self.data.get(ATTR_DEVICES, [])
@property
def auto_uart(self) -> bool:
"""Return True if we should map all UART device."""
return self.data[ATTR_AUTO_UART]
@property @property
def tmpfs(self) -> Optional[str]: def tmpfs(self) -> Optional[str]:
"""Return tmpfs of add-on.""" """Return tmpfs of add-on."""
@ -376,6 +372,16 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return True if the add-on access to GPIO interface.""" """Return True if the add-on access to GPIO interface."""
return self.data[ATTR_GPIO] return self.data[ATTR_GPIO]
@property
def with_usb(self) -> bool:
"""Return True if the add-on need USB access."""
return self.data[ATTR_USB]
@property
def with_uart(self) -> bool:
"""Return True if we should map all UART device."""
return self.data[ATTR_AUTO_UART]
@property @property
def with_udev(self) -> bool: def with_udev(self) -> bool:
"""Return True if the add-on have his own udev.""" """Return True if the add-on have his own udev."""

View File

@ -75,6 +75,7 @@ from ..const import (
ATTR_TMPFS, ATTR_TMPFS,
ATTR_UDEV, ATTR_UDEV,
ATTR_URL, ATTR_URL,
ATTR_USB,
ATTR_USER, ATTR_USER,
ATTR_UUID, ATTR_UUID,
ATTR_VERSION, ATTR_VERSION,
@ -226,6 +227,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(), vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(), vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(), vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),

View File

@ -81,6 +81,7 @@ from ..const import (
ATTR_STDIN, ATTR_STDIN,
ATTR_UDEV, ATTR_UDEV,
ATTR_URL, ATTR_URL,
ATTR_USB,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
ATTR_VIDEO, ATTR_VIDEO,
@ -237,6 +238,7 @@ class APIAddons(CoreSysAttributes):
ATTR_AUTH_API: addon.access_auth_api, ATTR_AUTH_API: addon.access_auth_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api, ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio, ATTR_GPIO: addon.with_gpio,
ATTR_USB: addon.with_usb,
ATTR_KERNEL_MODULES: addon.with_kernel_modules, ATTR_KERNEL_MODULES: addon.with_kernel_modules,
ATTR_DEVICETREE: addon.with_devicetree, ATTR_DEVICETREE: addon.with_devicetree,
ATTR_UDEV: addon.with_udev, ATTR_UDEV: addon.with_udev,

View File

@ -1,7 +1,7 @@
"""Init file for Supervisor hardware RESTful API.""" """Init file for Supervisor hardware RESTful API."""
import asyncio import asyncio
import logging import logging
from typing import Any, Awaitable, Dict from typing import Any, Awaitable, Dict, List
from aiohttp import web from aiohttp import web
@ -12,6 +12,7 @@ from ..const import (
ATTR_INPUT, ATTR_INPUT,
ATTR_OUTPUT, ATTR_OUTPUT,
ATTR_SERIAL, ATTR_SERIAL,
ATTR_USB,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .utils import api_process from .utils import api_process
@ -25,13 +26,24 @@ class APIHardware(CoreSysAttributes):
@api_process @api_process
async def info(self, request: web.Request) -> Dict[str, Any]: async def info(self, request: web.Request) -> Dict[str, Any]:
"""Show hardware info.""" """Show hardware info."""
serial: List[str] = []
# Create Serial list with device links
for device in self.sys_hardware.serial_devices:
serial.append(device.path.as_posix())
for link in device.links:
serial.append(link.as_posix())
return { return {
ATTR_SERIAL: list( ATTR_SERIAL: serial,
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
),
ATTR_INPUT: list(self.sys_hardware.input_devices), ATTR_INPUT: list(self.sys_hardware.input_devices),
ATTR_DISK: list(self.sys_hardware.disk_devices), ATTR_DISK: [
device.path.as_posix() for device in self.sys_hardware.disk_devices
],
ATTR_GPIO: list(self.sys_hardware.gpio_devices), ATTR_GPIO: list(self.sys_hardware.gpio_devices),
ATTR_USB: [
device.path.as_posix() for device in self.sys_hardware.usb_devices
],
ATTR_AUDIO: self.sys_hardware.audio_devices, ATTR_AUDIO: self.sys_hardware.audio_devices,
} }

View File

@ -42,5 +42,5 @@ class APIInfo(CoreSysAttributes):
ATTR_SUPPORTED: self.sys_core.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_config.timezone,
} }

View File

@ -131,8 +131,7 @@ class SecurityMiddleware(CoreSysAttributes):
request_from = self.sys_homeassistant request_from = self.sys_homeassistant
# Host # Host
# Remove machine_id handling later if all use new CLI if supervisor_token == self.sys_plugins.cli.supervisor_token:
if supervisor_token in (self.sys_machine_id, self.sys_plugins.cli.supervisor_token):
_LOGGER.debug("%s access from Host", request.path) _LOGGER.debug("%s access from Host", request.path)
request_from = self.sys_host request_from = self.sys_host

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 = "234" SUPERVISOR_VERSION = "235"
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"
@ -108,6 +108,7 @@ ATTR_PROVIDERS = "providers"
ATTR_VERSION = "version" ATTR_VERSION = "version"
ATTR_VERSION_LATEST = "version_latest" ATTR_VERSION_LATEST = "version_latest"
ATTR_AUTO_UART = "auto_uart" ATTR_AUTO_UART = "auto_uart"
ATTR_USB = "usb"
ATTR_LAST_BOOT = "last_boot" ATTR_LAST_BOOT = "last_boot"
ATTR_CHANNEL = "channel" ATTR_CHANNEL = "channel"
ATTR_NAME = "name" ATTR_NAME = "name"

View File

@ -7,7 +7,12 @@ import async_timeout
from .const import SOCKET_DBUS, SUPERVISED_SUPPORTED_OS, 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 (
DockerAPIError,
HassioError,
HomeAssistantError,
SupervisorUpdateError,
)
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -125,13 +130,8 @@ class Core(CoreSysAttributes):
if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS: if self.sys_host.info.operating_system not in SUPERVISED_SUPPORTED_OS:
self.supported = False self.supported = False
_LOGGER.error( _LOGGER.error(
"Using '%s' as the OS is not supported", "Detected unsupported OS: %s", self.sys_host.info.operating_system,
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 # Check all DBUS connectivity
if not self.sys_dbus.hostname.is_connected: if not self.sys_dbus.hostname.is_connected:
@ -145,9 +145,12 @@ class Core(CoreSysAttributes):
_LOGGER.error("Systemd DBUS is not connected") _LOGGER.error("Systemd DBUS is not connected")
# Check if image names from denylist exist # Check if image names from denylist exist
try:
if await self.sys_run_in_executor(self.sys_docker.check_denylist_images): if await self.sys_run_in_executor(self.sys_docker.check_denylist_images):
self.coresys.supported = False self.coresys.supported = False
self.healthy = False self.healthy = False
except DockerAPIError:
self.healthy = False
async def start(self): async def start(self):
"""Start Supervisor orchestration.""" """Start Supervisor orchestration."""
@ -157,7 +160,7 @@ class Core(CoreSysAttributes):
# Check if system is healthy # Check if system is healthy
if not self.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: if not self.healthy:
_LOGGER.critical( _LOGGER.critical(
"System running in a unhealthy state and need manual intervention!" "System running in a unhealthy state and need manual intervention!"
) )
@ -173,11 +176,12 @@ class Core(CoreSysAttributes):
_LOGGER.warning("Ignore Supervisor updates!") _LOGGER.warning("Ignore Supervisor updates!")
else: else:
await self.sys_supervisor.update() await self.sys_supervisor.update()
except SupervisorUpdateError: except SupervisorUpdateError as err:
_LOGGER.critical( _LOGGER.critical(
"Can't update supervisor! This will break some Add-ons or affect " "Can't update supervisor! This will break some Add-ons or affect "
"future version of Home Assistant!" "future version of Home Assistant!"
) )
self.sys_capture_exception(err)
# Start addon mark as initialize # Start addon mark as initialize
await self.sys_addons.boot(AddonStartup.INITIALIZE) await self.sys_addons.boot(AddonStartup.INITIALIZE)

View File

@ -90,11 +90,6 @@ class CoreSys:
return False return False
return self._updater.channel == UpdateChannels.DEV return self._updater.channel == UpdateChannels.DEV
@property
def timezone(self) -> str:
"""Return timezone."""
return self._config.timezone
@property @property
def loop(self) -> asyncio.BaseEventLoop: def loop(self) -> asyncio.BaseEventLoop:
"""Return loop object.""" """Return loop object."""
@ -459,16 +454,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_timezone(self) -> str:
"""Return timezone."""
return self.coresys.timezone
@property
def sys_machine_id(self) -> Optional[str]:
"""Return timezone."""
return self.coresys.machine_id
@property @property
def sys_loop(self) -> asyncio.BaseEventLoop: def sys_loop(self) -> asyncio.BaseEventLoop:
"""Return loop object.""" """Return loop object."""

View File

@ -8,6 +8,7 @@ from typing import Any, Dict, Optional
import attr import attr
import docker import docker
from packaging import version as pkg_version from packaging import version as pkg_version
import requests
from ..const import DNS_SUFFIX, DOCKER_IMAGE_DENYLIST, SOCKET_DOCKER from ..const import DNS_SUFFIX, DOCKER_IMAGE_DENYLIST, SOCKET_DOCKER
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
@ -128,7 +129,7 @@ class DockerAPI:
container = self.docker.containers.create( container = self.docker.containers.create(
f"{image}:{version}", use_config_proxy=False, **kwargs f"{image}:{version}", use_config_proxy=False, **kwargs
) )
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't create container from %s: %s", name, err) _LOGGER.error("Can't create container from %s: %s", name, err)
raise DockerAPIError() raise DockerAPIError()
@ -146,12 +147,12 @@ class DockerAPI:
# Run container # Run container
try: try:
container.start() container.start()
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't start %s: %s", name, err) _LOGGER.error("Can't start %s: %s", name, err)
raise DockerAPIError() raise DockerAPIError()
# Update metadata # Update metadata
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
container.reload() container.reload()
return container return container
@ -184,13 +185,13 @@ class DockerAPI:
result = container.wait() result = container.wait()
output = container.logs(stdout=stdout, stderr=stderr) output = container.logs(stdout=stdout, stderr=stderr)
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't execute command: %s", err) _LOGGER.error("Can't execute command: %s", err)
raise DockerAPIError() raise DockerAPIError()
finally: finally:
# cleanup container # cleanup container
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
container.remove(force=True) container.remove(force=True)
return CommandReturn(result.get("StatusCode"), output) return CommandReturn(result.get("StatusCode"), output)
@ -236,6 +237,8 @@ class DockerAPI:
def check_denylist_images(self) -> bool: def check_denylist_images(self) -> bool:
"""Return a boolean if the host has images in the denylist.""" """Return a boolean if the host has images in the denylist."""
denied_images = set() denied_images = set()
try:
for image in self.images.list(): for image in self.images.list():
for tag in image.tags: for tag in image.tags:
image_name = tag.split(":")[0] image_name = tag.split(":")[0]
@ -244,6 +247,9 @@ class DockerAPI:
and image_name not in denied_images and image_name not in denied_images
): ):
denied_images.add(image_name) denied_images.add(image_name)
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Corrupt docker overlayfs detect: %s", err)
raise DockerAPIError()
if not denied_images: if not denied_images:
return False return False

View File

@ -116,7 +116,7 @@ class DockerAddon(DockerInterface):
return { return {
**addon_env, **addon_env,
ENV_TIME: self.sys_timezone, ENV_TIME: self.sys_config.timezone,
ENV_TOKEN: self.addon.supervisor_token, ENV_TOKEN: self.addon.supervisor_token,
ENV_TOKEN_OLD: self.addon.supervisor_token, ENV_TOKEN_OLD: self.addon.supervisor_token,
} }
@ -127,21 +127,22 @@ class DockerAddon(DockerInterface):
devices = [] devices = []
# Extend add-on config # Extend add-on config
if self.addon.devices: for device in self.addon.devices:
devices.extend(self.addon.devices) if not Path(device.split(":")[0]).exists():
continue
devices.append(device)
# Auto mapping UART devices # Auto mapping UART devices
if self.addon.auto_uart: if self.addon.with_uart:
for device in self.sys_hardware.serial_devices:
devices.append(f"{device.path.as_posix()}:{device.path.as_posix()}:rwm")
if self.addon.with_udev: if self.addon.with_udev:
serial_devs = self.sys_hardware.serial_devices continue
else: for device_link in device.links:
serial_devs = ( devices.append(
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id f"{device_link.as_posix()}:{device_link.as_posix()}:rwm"
) )
for device in serial_devs:
devices.append(f"{device}:{device}:rwm")
# Use video devices # Use video devices
if self.addon.with_video: if self.addon.with_video:
for device in self.sys_hardware.video_devices: for device in self.sys_hardware.video_devices:
@ -286,6 +287,10 @@ class DockerAddon(DockerInterface):
} }
) )
# USB support
if self.addon.with_usb and self.sys_hardware.usb_devices:
volumes.update({"/dev/bus/usb": {"bind": "/dev/bus/usb", "mode": "rw"}})
# Kernel Modules support # Kernel Modules support
if self.addon.with_kernel_modules: if self.addon.with_kernel_modules:
volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}}) volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}})
@ -334,7 +339,6 @@ class DockerAddon(DockerInterface):
_LOGGER.warning("%s run with disabled protected mode!", self.addon.name) _LOGGER.warning("%s run with disabled protected mode!", self.addon.name)
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -396,7 +400,7 @@ class DockerAddon(DockerInterface):
# Update meta data # Update meta data
self._meta = image.attrs self._meta = image.attrs
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't build %s:%s: %s", self.image, tag, err) _LOGGER.error("Can't build %s:%s: %s", self.image, tag, err)
raise DockerAPIError() raise DockerAPIError()
@ -414,7 +418,7 @@ class DockerAddon(DockerInterface):
""" """
try: try:
image = self.sys_docker.api.get_image(f"{self.image}:{self.version}") image = self.sys_docker.api.get_image(f"{self.image}:{self.version}")
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't fetch image %s: %s", self.image, err) _LOGGER.error("Can't fetch image %s: %s", self.image, err)
raise DockerAPIError() raise DockerAPIError()
@ -423,7 +427,7 @@ class DockerAddon(DockerInterface):
with tar_file.open("wb") as write_tar: with tar_file.open("wb") as write_tar:
for chunk in image: for chunk in image:
write_tar.write(chunk) write_tar.write(chunk)
except (OSError, requests.exceptions.ReadTimeout) as err: except (OSError, requests.RequestException) as err:
_LOGGER.error("Can't write tar file %s: %s", tar_file, err) _LOGGER.error("Can't write tar file %s: %s", tar_file, err)
raise DockerAPIError() raise DockerAPIError()
@ -471,7 +475,7 @@ class DockerAddon(DockerInterface):
# Load needed docker objects # Load needed docker objects
container = self.sys_docker.containers.get(self.name) container = self.sys_docker.containers.get(self.name)
socket = container.attach_socket(params={"stdin": 1, "stream": 1}) socket = container.attach_socket(params={"stdin": 1, "stream": 1})
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't attach to %s stdin: %s", self.name, err) _LOGGER.error("Can't attach to %s stdin: %s", self.name, err)
raise DockerAPIError() raise DockerAPIError()

View File

@ -1,12 +1,10 @@
"""Audio docker object.""" """Audio docker object."""
from contextlib import suppress
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from ..const import ENV_TIME, MACHINE_ID from ..const import ENV_TIME, MACHINE_ID
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -56,7 +54,6 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
return return
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -69,7 +66,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
hostname=self.name.replace("_", "-"), hostname=self.name.replace("_", "-"),
detach=True, detach=True,
privileged=True, privileged=True,
environment={ENV_TIME: self.sys_timezone}, environment={ENV_TIME: self.sys_config.timezone},
volumes=self.volumes, volumes=self.volumes,
) )

View File

@ -1,10 +1,8 @@
"""HA Cli docker object.""" """HA Cli docker object."""
from contextlib import suppress
import logging import logging
from ..const import ENV_TIME, ENV_TOKEN from ..const import ENV_TIME, ENV_TOKEN
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -34,7 +32,6 @@ class DockerCli(DockerInterface, CoreSysAttributes):
return return
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -50,7 +47,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
detach=True, detach=True,
extra_hosts={"supervisor": self.sys_docker.network.supervisor}, extra_hosts={"supervisor": self.sys_docker.network.supervisor},
environment={ environment={
ENV_TIME: self.sys_timezone, ENV_TIME: self.sys_config.timezone,
ENV_TOKEN: self.sys_plugins.cli.supervisor_token, ENV_TOKEN: self.sys_plugins.cli.supervisor_token,
}, },
) )

View File

@ -1,10 +1,8 @@
"""DNS docker object.""" """DNS docker object."""
from contextlib import suppress
import logging import logging
from ..const import ENV_TIME from ..const import ENV_TIME
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -34,7 +32,6 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
return return
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -47,7 +44,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
name=self.name, name=self.name,
hostname=self.name.replace("_", "-"), hostname=self.name.replace("_", "-"),
detach=True, detach=True,
environment={ENV_TIME: self.sys_timezone}, environment={ENV_TIME: self.sys_config.timezone},
volumes={ volumes={
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"} str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"}
}, },

View File

@ -1,10 +1,10 @@
"""Init file for Supervisor Docker object.""" """Init file for Supervisor Docker object."""
from contextlib import suppress
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging import logging
from typing import Awaitable, Dict, Optional from typing import Awaitable, Dict, Optional
import docker 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_OLD, LABEL_MACHINE, MACHINE_ID
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
@ -98,7 +98,6 @@ class DockerHomeAssistant(DockerInterface):
return return
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -115,7 +114,7 @@ class DockerHomeAssistant(DockerInterface):
environment={ environment={
"HASSIO": self.sys_docker.network.supervisor, "HASSIO": self.sys_docker.network.supervisor,
"SUPERVISOR": self.sys_docker.network.supervisor, "SUPERVISOR": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone, ENV_TIME: self.sys_config.timezone,
ENV_TOKEN: self.sys_homeassistant.supervisor_token, ENV_TOKEN: self.sys_homeassistant.supervisor_token,
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token, ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
}, },
@ -150,7 +149,7 @@ class DockerHomeAssistant(DockerInterface):
"mode": "ro", "mode": "ro",
}, },
}, },
environment={ENV_TIME: self.sys_timezone}, environment={ENV_TIME: self.sys_config.timezone},
) )
def is_initialize(self) -> Awaitable[bool]: def is_initialize(self) -> Awaitable[bool]:
@ -167,8 +166,10 @@ class DockerHomeAssistant(DockerInterface):
docker_image = self.sys_docker.images.get( docker_image = self.sys_docker.images.get(
f"{self.image}:{self.sys_homeassistant.version}" f"{self.image}:{self.sys_homeassistant.version}"
) )
except docker.errors.DockerException: except docker.errors.NotFound:
return False return False
except (docker.errors.DockerException, requests.RequestException):
return DockerAPIError()
# we run on an old image, stop and start it # we run on an old image, stop and start it
if docker_container.image.id != docker_image.id: if docker_container.image.id != docker_image.id:

View File

@ -5,6 +5,7 @@ import logging
from typing import Any, Awaitable, Dict, List, Optional from typing import Any, Awaitable, Dict, List, Optional
import docker import docker
import requests
from . import CommandReturn from . import CommandReturn
from ..const import LABEL_ARCH, LABEL_VERSION from ..const import LABEL_ARCH, LABEL_VERSION
@ -107,6 +108,10 @@ class DockerInterface(CoreSysAttributes):
free_space, free_space,
) )
raise DockerAPIError() raise DockerAPIError()
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Unknown error with %s:%s -> %s", image, tag, err)
self.sys_capture_exception(err)
raise DockerAPIError()
else: else:
self._meta = docker_image.attrs self._meta = docker_image.attrs
@ -119,7 +124,7 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
self.sys_docker.images.get(f"{self.image}:{self.version}") self.sys_docker.images.get(f"{self.image}:{self.version}")
return True return True
return False return False
@ -138,8 +143,10 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except docker.errors.NotFound:
return False return False
except (docker.errors.DockerException, requests.RequestException):
raise DockerAPIError()
return docker_container.status == "running" return docker_container.status == "running"
@ -153,10 +160,10 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
self._meta = self.sys_docker.containers.get(self.name).attrs self._meta = self.sys_docker.containers.get(self.name).attrs
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
if not self._meta and self.image: if not self._meta and self.image:
self._meta = self.sys_docker.images.get(f"{self.image}:{tag}").attrs self._meta = self.sys_docker.images.get(f"{self.image}:{tag}").attrs
@ -189,16 +196,18 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except docker.errors.NotFound:
return
except (docker.errors.DockerException, requests.RequestException):
raise DockerAPIError() raise DockerAPIError()
if docker_container.status == "running": if docker_container.status == "running":
_LOGGER.info("Stop %s application", self.name) _LOGGER.info("Stop %s application", self.name)
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
docker_container.stop(timeout=self.timeout) docker_container.stop(timeout=self.timeout)
if remove_container: if remove_container:
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
_LOGGER.info("Clean %s application", self.name) _LOGGER.info("Clean %s application", self.name)
docker_container.remove(force=True) docker_container.remove(force=True)
@ -214,13 +223,14 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
_LOGGER.error("%s not found for starting up", self.name)
raise DockerAPIError() raise DockerAPIError()
_LOGGER.info("Start %s", self.name) _LOGGER.info("Start %s", self.name)
try: try:
docker_container.start() docker_container.start()
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't start %s: %s", self.name, err) _LOGGER.error("Can't start %s: %s", self.name, err)
raise DockerAPIError() raise DockerAPIError()
@ -249,7 +259,7 @@ class DockerInterface(CoreSysAttributes):
image=f"{self.image}:{self.version}", force=True image=f"{self.image}:{self.version}", force=True
) )
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.warning("Can't remove image %s: %s", self.image, err) _LOGGER.warning("Can't remove image %s: %s", self.image, err)
raise DockerAPIError() raise DockerAPIError()
@ -296,12 +306,12 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
return b"" return b""
try: try:
return docker_container.logs(tail=100, stdout=True, stderr=True) return docker_container.logs(tail=100, stdout=True, stderr=True)
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.warning("Can't grep logs from %s: %s", self.image, err) _LOGGER.warning("Can't grep logs from %s: %s", self.image, err)
return b"" return b""
@ -318,7 +328,7 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
origin = self.sys_docker.images.get(f"{self.image}:{self.version}") origin = self.sys_docker.images.get(f"{self.image}:{self.version}")
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
_LOGGER.warning("Can't find %s for cleanup", self.image) _LOGGER.warning("Can't find %s for cleanup", self.image)
raise DockerAPIError() raise DockerAPIError()
@ -327,7 +337,7 @@ class DockerInterface(CoreSysAttributes):
if origin.id == image.id: if origin.id == image.id:
continue continue
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
_LOGGER.info("Cleanup images: %s", image.tags) _LOGGER.info("Cleanup images: %s", image.tags)
self.sys_docker.images.remove(image.id, force=True) self.sys_docker.images.remove(image.id, force=True)
@ -336,7 +346,7 @@ class DockerInterface(CoreSysAttributes):
return return
for image in self.sys_docker.images.list(name=old_image): for image in self.sys_docker.images.list(name=old_image):
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException, requests.RequestException):
_LOGGER.info("Cleanup images: %s", image.tags) _LOGGER.info("Cleanup images: %s", image.tags)
self.sys_docker.images.remove(image.id, force=True) self.sys_docker.images.remove(image.id, force=True)
@ -352,13 +362,13 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
container = self.sys_docker.containers.get(self.name) container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
raise DockerAPIError() raise DockerAPIError()
_LOGGER.info("Restart %s", self.image) _LOGGER.info("Restart %s", self.image)
try: try:
container.restart(timeout=self.timeout) container.restart(timeout=self.timeout)
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.warning("Can't restart %s: %s", self.image, err) _LOGGER.warning("Can't restart %s: %s", self.image, err)
raise DockerAPIError() raise DockerAPIError()
@ -385,13 +395,13 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
raise DockerAPIError() raise DockerAPIError()
try: try:
stats = docker_container.stats(stream=False) stats = docker_container.stats(stream=False)
return DockerStats(stats) return DockerStats(stats)
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't read stats from %s: %s", self.name, err) _LOGGER.error("Can't read stats from %s: %s", self.name, err)
raise DockerAPIError() raise DockerAPIError()
@ -409,7 +419,7 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
return False return False
# container is not running # container is not running

View File

@ -1,10 +1,8 @@
"""HA Cli docker object.""" """HA Cli docker object."""
from contextlib import suppress
import logging import logging
from ..const import ENV_TIME from ..const import ENV_TIME
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -34,7 +32,6 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
return return
# Cleanup # Cleanup
with suppress(DockerAPIError):
self._stop() self._stop()
# Create & Run container # Create & Run container
@ -47,7 +44,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
network_mode="host", network_mode="host",
detach=True, detach=True,
extra_hosts={"supervisor": self.sys_docker.network.supervisor}, extra_hosts={"supervisor": self.sys_docker.network.supervisor},
environment={ENV_TIME: self.sys_timezone}, environment={ENV_TIME: self.sys_config.timezone},
) )
self._meta = docker_container.attrs self._meta = docker_container.attrs

View File

@ -5,6 +5,7 @@ import logging
from typing import List, Optional from typing import List, Optional
import docker import docker
import requests
from ..const import DOCKER_NETWORK, DOCKER_NETWORK_MASK, DOCKER_NETWORK_RANGE from ..const import DOCKER_NETWORK, DOCKER_NETWORK_MASK, DOCKER_NETWORK_RANGE
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
@ -35,9 +36,11 @@ class DockerNetwork:
for cid, data in self.network.attrs.get("Containers", {}).items(): for cid, data in self.network.attrs.get("Containers", {}).items():
try: try:
containers.append(self.docker.containers.get(cid)) containers.append(self.docker.containers.get(cid))
except docker.errors.APIError as err: except docker.errors.NotFound:
_LOGGER.warning("Docker network is corrupt! %s - run autofix", err) _LOGGER.warning("Docker network is corrupt! %s - run autofix", cid)
self.stale_cleanup(data.get("Name", cid)) self.stale_cleanup(data.get("Name", cid))
except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Unknown error with container lookup %s", err)
return containers return containers
@ -132,5 +135,5 @@ class DockerNetwork:
Fix: https://github.com/moby/moby/issues/23302 Fix: https://github.com/moby/moby/issues/23302
""" """
with suppress(docker.errors.APIError): with suppress(docker.errors.DockerException, requests.RequestException):
self.network.disconnect(container_name, force=True) self.network.disconnect(container_name, force=True)

View File

@ -5,6 +5,7 @@ import os
from typing import Awaitable from typing import Awaitable
import docker import docker
import requests
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
@ -38,7 +39,7 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException: except (docker.errors.DockerException, requests.RequestException):
raise DockerAPIError() raise DockerAPIError()
self._meta = docker_container.attrs self._meta = docker_container.attrs
@ -74,7 +75,7 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
docker_container.image.tag(self.image, tag=self.version) docker_container.image.tag(self.image, tag=self.version)
docker_container.image.tag(self.image, tag="latest") docker_container.image.tag(self.image, tag="latest")
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't retag supervisor version: %s", err) _LOGGER.error("Can't retag supervisor version: %s", err)
raise DockerAPIError() raise DockerAPIError()
@ -101,6 +102,6 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
continue continue
docker_image.tag(start_image, start_tag) docker_image.tag(start_image, start_tag)
except docker.errors.DockerException as err: except (docker.errors.DockerException, requests.RequestException) as err:
_LOGGER.error("Can't fix start tag: %s", err) _LOGGER.error("Can't fix start tag: %s", err)
raise DockerAPIError() raise DockerAPIError()

View File

@ -38,25 +38,40 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP): if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP):
return event return event
# List installed addons
installed_addons = [
{"slug": addon.slug, "repository": addon.repository, "name": addon.name}
for addon in coresys.addons.installed
]
# Update information # Update information
event.setdefault("extra", {}).update( event.setdefault("user", {}).update({"id": coresys.machine_id})
event.setdefault("contexts", {}).update(
{ {
"supervisor": { "supervisor": {
"machine": coresys.machine,
"arch": coresys.arch.default,
"docker": coresys.docker.info.version,
"channel": coresys.updater.channel, "channel": coresys.updater.channel,
"supervisor": coresys.supervisor.version, "installed_addons": installed_addons,
"os": coresys.hassos.version, "repositories": coresys.config.addons_repositories,
},
"host": {
"arch": coresys.arch.default,
"board": coresys.hassos.board,
"deployment": coresys.host.info.deployment,
"disk_free_space": coresys.host.info.free_space,
"host": coresys.host.info.operating_system, "host": coresys.host.info.operating_system,
"kernel": coresys.host.info.kernel, "kernel": coresys.host.info.kernel,
"core": coresys.homeassistant.version, "machine": coresys.machine,
},
"versions": {
"audio": coresys.plugins.audio.version, "audio": coresys.plugins.audio.version,
"dns": coresys.plugins.dns.version,
"multicast": coresys.plugins.multicast.version,
"cli": coresys.plugins.cli.version, "cli": coresys.plugins.cli.version,
"disk_free_space": coresys.host.info.free_space, "core": coresys.homeassistant.version,
} "dns": coresys.plugins.dns.version,
"docker": coresys.docker.info.version,
"multicast": coresys.plugins.multicast.version,
"os": coresys.hassos.version,
"supervisor": coresys.supervisor.version,
},
} }
) )
event.setdefault("tags", []).extend( event.setdefault("tags", []).extend(
@ -86,4 +101,5 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
if key in [hdrs.HOST, hdrs.X_FORWARDED_HOST]: if key in [hdrs.HOST, hdrs.X_FORWARDED_HOST]:
event["request"]["headers"][i] = [key, "example.com"] event["request"]["headers"][i] = [key, "example.com"]
return event return event

View File

@ -31,13 +31,15 @@ RE_TTY: re.Pattern = re.compile(r"tty[A-Z]+")
RE_VIDEO_DEVICES = re.compile(r"^(?:vchiq|cec\d+|video\d+)") RE_VIDEO_DEVICES = re.compile(r"^(?:vchiq|cec\d+|video\d+)")
@attr.s(frozen=True) @attr.s(slots=True, frozen=True)
class Device: class Device:
"""Represent a device.""" """Represent a device."""
name: str = attr.ib() name: str = attr.ib()
path: Path = attr.ib() path: Path = attr.ib()
subsystem: str = attr.ib()
links: List[Path] = attr.ib() links: List[Path] = attr.ib()
attributes: Dict[str, str] = attr.ib()
class Hardware: class Hardware:
@ -62,7 +64,9 @@ class Hardware:
Device( Device(
device.sys_name, device.sys_name,
Path(device.device_node), Path(device.device_node),
device.subsystem,
[Path(node) for node in device.device_links], [Path(node) for node in device.device_links],
{attr: device.properties[attr] for attr in device.properties},
) )
) )
@ -81,28 +85,30 @@ class Hardware:
return dev_list return dev_list
@property @property
def serial_devices(self) -> Set[str]: def serial_devices(self) -> List[Device]:
"""Return all serial and connected devices.""" """Return all serial and connected devices."""
dev_list: Set[str] = set() dev_list: List[Device] = []
for device in self.context.list_devices(subsystem="tty"): for device in self.devices:
if "ID_VENDOR" in device.properties or RE_TTY.search(device.device_node): if device.subsystem != "tty" or (
dev_list.add(device.device_node) "ID_VENDOR" not in device.attributes
and not RE_TTY.search(str(device.path))
):
continue
# Cleanup not usable device links
for link in device.links.copy():
if link.match("/dev/serial/by-id/*"):
continue
device.links.remove(link)
dev_list.append(device)
return dev_list return dev_list
@property @property
def serial_by_id(self) -> Set[str]: def usb_devices(self) -> List[Device]:
"""Return all /dev/serial/by-id for serial devices.""" """Return all usb and connected devices."""
dev_list: Set[str] = set() return [device for device in self.devices if device.subsystem == "usb"]
for device in self.context.list_devices(subsystem="tty"):
if "ID_VENDOR" in device.properties or RE_TTY.search(device.device_node):
# Add /dev/serial/by-id devlink for current device
for dev_link in device.device_links:
if not dev_link.startswith("/dev/serial/by-id"):
continue
dev_list.add(dev_link)
return dev_list
@property @property
def input_devices(self) -> Set[str]: def input_devices(self) -> Set[str]:
@ -115,12 +121,13 @@ class Hardware:
return dev_list return dev_list
@property @property
def disk_devices(self) -> Set[str]: def disk_devices(self) -> List[Device]:
"""Return all disk devices.""" """Return all disk devices."""
dev_list: Set[str] = set() dev_list: List[Device] = []
for device in self.context.list_devices(subsystem="block"): for device in self.devices:
if "ID_NAME" in device.properties: if device.subsystem != "block" or "ID_NAME" not in device.attributes:
dev_list.add(device.device_node) continue
dev_list.append(device)
return dev_list return dev_list

View File

@ -1,5 +1,6 @@
"""Common test functions.""" """Common test functions."""
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from uuid import uuid4
import pytest import pytest
@ -39,6 +40,9 @@ async def coresys(loop, docker):
coresys_obj.ingress.save_data = MagicMock() coresys_obj.ingress.save_data = MagicMock()
coresys_obj.arch._default_arch = "amd64" coresys_obj.arch._default_arch = "amd64"
coresys_obj._machine = "qemux86-64"
coresys_obj._machine_id = uuid4()
yield coresys_obj yield coresys_obj

View File

@ -1,7 +1,7 @@
"""Test sentry data filter.""" """Test sentry data filter."""
from unittest.mock import patch from unittest.mock import patch
from supervisor.const import CoreStates from supervisor.const import SUPERVISOR_VERSION, CoreStates
from supervisor.exceptions import AddonConfigurationError from supervisor.exceptions import AddonConfigurationError
from supervisor.misc.filter import filter_data from supervisor.misc.filter import filter_data
@ -58,7 +58,10 @@ def test_defaults(coresys):
filtered = filter_data(coresys, SAMPLE_EVENT, {}) filtered = filter_data(coresys, SAMPLE_EVENT, {})
assert ["installation_type", "supervised"] in filtered["tags"] assert ["installation_type", "supervised"] in filtered["tags"]
assert filtered["extra"]["supervisor"]["arch"] == "amd64" assert filtered["contexts"]["host"]["arch"] == "amd64"
assert filtered["contexts"]["host"]["machine"] == "qemux86-64"
assert filtered["contexts"]["versions"]["supervisor"] == SUPERVISOR_VERSION
assert filtered["user"]["id"] == coresys.machine_id
def test_sanitize(coresys): def test_sanitize(coresys):

View File

@ -16,10 +16,10 @@ def test_video_devices():
"""Test video device filter.""" """Test video device filter."""
system = Hardware() system = Hardware()
device_list = [ device_list = [
Device("test-dev", Path("/dev/test-dev"), []), Device("test-dev", Path("/dev/test-dev"), "xy", [], {}),
Device("vchiq", Path("/dev/vchiq"), []), Device("vchiq", Path("/dev/vchiq"), "xy", [], {}),
Device("cec0", Path("/dev/cec0"), []), Device("cec0", Path("/dev/cec0"), "xy", [], {}),
Device("video1", Path("/dev/video1"), []), Device("video1", Path("/dev/video1"), "xy", [], {}),
] ]
with patch( with patch(
@ -27,10 +27,80 @@ def test_video_devices():
) as mock_device: ) as mock_device:
mock_device.return_value = device_list mock_device.return_value = device_list
assert system.video_devices == [ assert [device.name for device in system.video_devices] == [
Device("vchiq", Path("/dev/vchiq"), []), "vchiq",
Device("cec0", Path("/dev/cec0"), []), "cec0",
Device("video1", Path("/dev/video1"), []), "video1",
]
def test_serial_devices():
"""Test serial device filter."""
system = Hardware()
device_list = [
Device("ttyACM0", Path("/dev/ttyACM0"), "tty", [], {"ID_VENDOR": "xy"}),
Device(
"ttyUSB0",
Path("/dev/ttyUSB0"),
"tty",
[Path("/dev/ttyS1"), Path("/dev/serial/by-id/xyx")],
{"ID_VENDOR": "xy"},
),
Device("ttyS0", Path("/dev/ttyS0"), "tty", [], {}),
Device("video1", Path("/dev/video1"), "misc", [], {"ID_VENDOR": "xy"}),
]
with patch(
"supervisor.misc.hardware.Hardware.devices", new_callable=PropertyMock
) as mock_device:
mock_device.return_value = device_list
assert [(device.name, device.links) for device in system.serial_devices] == [
("ttyACM0", []),
("ttyUSB0", [Path("/dev/serial/by-id/xyx")]),
("ttyS0", []),
]
def test_usb_devices():
"""Test usb device filter."""
system = Hardware()
device_list = [
Device("usb1", Path("/dev/bus/usb/1/1"), "usb", [], {}),
Device("usb2", Path("/dev/bus/usb/2/1"), "usb", [], {}),
Device("cec0", Path("/dev/cec0"), "xy", [], {}),
Device("video1", Path("/dev/video1"), "xy", [], {}),
]
with patch(
"supervisor.misc.hardware.Hardware.devices", new_callable=PropertyMock
) as mock_device:
mock_device.return_value = device_list
assert [device.name for device in system.usb_devices] == [
"usb1",
"usb2",
]
def test_block_devices():
"""Test usb device filter."""
system = Hardware()
device_list = [
Device("sda", Path("/dev/sda"), "block", [], {"ID_NAME": "xy"}),
Device("sdb", Path("/dev/sdb"), "block", [], {"ID_NAME": "xy"}),
Device("cec0", Path("/dev/cec0"), "xy", [], {}),
Device("video1", Path("/dev/video1"), "xy", [], {"ID_NAME": "xy"}),
]
with patch(
"supervisor.misc.hardware.Hardware.devices", new_callable=PropertyMock
) as mock_device:
mock_device.return_value = device_list
assert [device.name for device in system.disk_devices] == [
"sda",
"sdb",
] ]