mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-24 09:36:31 +00:00
commit
e9802f92c9
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -429,4 +429,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- 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
21
.github/workflows/sentry.yaml
vendored
Normal 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
1
API.md
@ -535,6 +535,7 @@ Get all available add-ons.
|
||||
"stdin": "bool",
|
||||
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
||||
"gpio": "bool",
|
||||
"usb": "[physical_path_to_usb_device]",
|
||||
"kernel_modules": "bool",
|
||||
"devicetree": "bool",
|
||||
"docker_api": "bool",
|
||||
|
@ -14,6 +14,6 @@ pulsectl==20.5.1
|
||||
pytz==2020.1
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.16.4
|
||||
sentry-sdk==0.16.5
|
||||
uvloop==0.14.0
|
||||
voluptuous==0.11.7
|
||||
|
@ -7,7 +7,7 @@ pre-commit==2.6.0
|
||||
pydocstyle==5.0.2
|
||||
pylint==2.5.3
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-cov==2.10.0
|
||||
pytest-cov==2.10.1
|
||||
pytest-timeout==1.4.2
|
||||
pytest==6.0.1
|
||||
pyupgrade==2.7.2
|
||||
|
@ -58,6 +58,7 @@ from ..const import (
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_VERSION,
|
||||
ATTR_VIDEO,
|
||||
ATTR_WEBUI,
|
||||
@ -292,11 +293,6 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return devices of add-on."""
|
||||
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
|
||||
def tmpfs(self) -> Optional[str]:
|
||||
"""Return tmpfs of add-on."""
|
||||
@ -376,6 +372,16 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on access to GPIO interface."""
|
||||
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
|
||||
def with_udev(self) -> bool:
|
||||
"""Return True if the add-on have his own udev."""
|
||||
|
@ -75,6 +75,7 @@ from ..const import (
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
@ -226,6 +227,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_VIDEO, 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_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||
|
@ -81,6 +81,7 @@ from ..const import (
|
||||
ATTR_STDIN,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
ATTR_VIDEO,
|
||||
@ -237,6 +238,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_AUTH_API: addon.access_auth_api,
|
||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_USB: addon.with_usb,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Init file for Supervisor hardware RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict
|
||||
from typing import Any, Awaitable, Dict, List
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
@ -12,6 +12,7 @@ from ..const import (
|
||||
ATTR_INPUT,
|
||||
ATTR_OUTPUT,
|
||||
ATTR_SERIAL,
|
||||
ATTR_USB,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .utils import api_process
|
||||
@ -25,13 +26,24 @@ class APIHardware(CoreSysAttributes):
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""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 {
|
||||
ATTR_SERIAL: list(
|
||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
||||
),
|
||||
ATTR_SERIAL: serial,
|
||||
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_USB: [
|
||||
device.path.as_posix() for device in self.sys_hardware.usb_devices
|
||||
],
|
||||
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
||||
}
|
||||
|
||||
|
@ -42,5 +42,5 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_LOGGING: self.sys_config.logging,
|
||||
ATTR_TIMEZONE: self.sys_timezone,
|
||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||
}
|
||||
|
@ -131,8 +131,7 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
request_from = self.sys_homeassistant
|
||||
|
||||
# Host
|
||||
# Remove machine_id handling later if all use new CLI
|
||||
if supervisor_token in (self.sys_machine_id, self.sys_plugins.cli.supervisor_token):
|
||||
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
||||
_LOGGER.debug("%s access from Host", request.path)
|
||||
request_from = self.sys_host
|
||||
|
||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
SUPERVISOR_VERSION = "234"
|
||||
SUPERVISOR_VERSION = "235"
|
||||
|
||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json"
|
||||
@ -108,6 +108,7 @@ ATTR_PROVIDERS = "providers"
|
||||
ATTR_VERSION = "version"
|
||||
ATTR_VERSION_LATEST = "version_latest"
|
||||
ATTR_AUTO_UART = "auto_uart"
|
||||
ATTR_USB = "usb"
|
||||
ATTR_LAST_BOOT = "last_boot"
|
||||
ATTR_CHANNEL = "channel"
|
||||
ATTR_NAME = "name"
|
||||
|
@ -7,7 +7,12 @@ import async_timeout
|
||||
|
||||
from .const import SOCKET_DBUS, SUPERVISED_SUPPORTED_OS, AddonStartup, CoreStates
|
||||
from .coresys import CoreSys, CoreSysAttributes
|
||||
from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError
|
||||
from .exceptions import (
|
||||
DockerAPIError,
|
||||
HassioError,
|
||||
HomeAssistantError,
|
||||
SupervisorUpdateError,
|
||||
)
|
||||
|
||||
_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:
|
||||
self.supported = False
|
||||
_LOGGER.error(
|
||||
"Using '%s' as the OS is not supported",
|
||||
self.sys_host.info.operating_system,
|
||||
"Detected unsupported OS: %s", 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:
|
||||
@ -145,8 +145,11 @@ class Core(CoreSysAttributes):
|
||||
_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
|
||||
try:
|
||||
if await self.sys_run_in_executor(self.sys_docker.check_denylist_images):
|
||||
self.coresys.supported = False
|
||||
self.healthy = False
|
||||
except DockerAPIError:
|
||||
self.healthy = False
|
||||
|
||||
async def start(self):
|
||||
@ -157,7 +160,7 @@ class Core(CoreSysAttributes):
|
||||
# Check if system is healthy
|
||||
if not self.supported:
|
||||
_LOGGER.critical("System running in a unsupported environment!")
|
||||
elif not self.healthy:
|
||||
if not self.healthy:
|
||||
_LOGGER.critical(
|
||||
"System running in a unhealthy state and need manual intervention!"
|
||||
)
|
||||
@ -173,11 +176,12 @@ class Core(CoreSysAttributes):
|
||||
_LOGGER.warning("Ignore Supervisor updates!")
|
||||
else:
|
||||
await self.sys_supervisor.update()
|
||||
except SupervisorUpdateError:
|
||||
except SupervisorUpdateError as err:
|
||||
_LOGGER.critical(
|
||||
"Can't update supervisor! This will break some Add-ons or affect "
|
||||
"future version of Home Assistant!"
|
||||
)
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
# Start addon mark as initialize
|
||||
await self.sys_addons.boot(AddonStartup.INITIALIZE)
|
||||
|
@ -90,11 +90,6 @@ class CoreSys:
|
||||
return False
|
||||
return self._updater.channel == UpdateChannels.DEV
|
||||
|
||||
@property
|
||||
def timezone(self) -> str:
|
||||
"""Return timezone."""
|
||||
return self._config.timezone
|
||||
|
||||
@property
|
||||
def loop(self) -> asyncio.BaseEventLoop:
|
||||
"""Return loop object."""
|
||||
@ -459,16 +454,6 @@ class CoreSysAttributes:
|
||||
"""Return True if we run dev mode."""
|
||||
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
|
||||
def sys_loop(self) -> asyncio.BaseEventLoop:
|
||||
"""Return loop object."""
|
||||
|
@ -8,6 +8,7 @@ from typing import Any, Dict, Optional
|
||||
import attr
|
||||
import docker
|
||||
from packaging import version as pkg_version
|
||||
import requests
|
||||
|
||||
from ..const import DNS_SUFFIX, DOCKER_IMAGE_DENYLIST, SOCKET_DOCKER
|
||||
from ..exceptions import DockerAPIError
|
||||
@ -128,7 +129,7 @@ class DockerAPI:
|
||||
container = self.docker.containers.create(
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -146,12 +147,12 @@ class DockerAPI:
|
||||
# Run container
|
||||
try:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
# Update metadata
|
||||
with suppress(docker.errors.DockerException):
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
container.reload()
|
||||
|
||||
return container
|
||||
@ -184,13 +185,13 @@ class DockerAPI:
|
||||
result = container.wait()
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
finally:
|
||||
# cleanup container
|
||||
with suppress(docker.errors.DockerException):
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
container.remove(force=True)
|
||||
|
||||
return CommandReturn(result.get("StatusCode"), output)
|
||||
@ -236,14 +237,19 @@ class DockerAPI:
|
||||
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)
|
||||
|
||||
try:
|
||||
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)
|
||||
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||
_LOGGER.error("Corrupt docker overlayfs detect: %s", err)
|
||||
raise DockerAPIError()
|
||||
|
||||
if not denied_images:
|
||||
return False
|
||||
|
@ -116,7 +116,7 @@ class DockerAddon(DockerInterface):
|
||||
|
||||
return {
|
||||
**addon_env,
|
||||
ENV_TIME: self.sys_timezone,
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.addon.supervisor_token,
|
||||
ENV_TOKEN_OLD: self.addon.supervisor_token,
|
||||
}
|
||||
@ -127,20 +127,21 @@ class DockerAddon(DockerInterface):
|
||||
devices = []
|
||||
|
||||
# Extend add-on config
|
||||
if self.addon.devices:
|
||||
devices.extend(self.addon.devices)
|
||||
for device in self.addon.devices:
|
||||
if not Path(device.split(":")[0]).exists():
|
||||
continue
|
||||
devices.append(device)
|
||||
|
||||
# Auto mapping UART devices
|
||||
if self.addon.auto_uart:
|
||||
if self.addon.with_udev:
|
||||
serial_devs = self.sys_hardware.serial_devices
|
||||
else:
|
||||
serial_devs = (
|
||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
||||
)
|
||||
|
||||
for device in serial_devs:
|
||||
devices.append(f"{device}:{device}:rwm")
|
||||
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:
|
||||
continue
|
||||
for device_link in device.links:
|
||||
devices.append(
|
||||
f"{device_link.as_posix()}:{device_link.as_posix()}:rwm"
|
||||
)
|
||||
|
||||
# Use video devices
|
||||
if self.addon.with_video:
|
||||
@ -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
|
||||
if self.addon.with_kernel_modules:
|
||||
volumes.update({"/lib/modules": {"bind": "/lib/modules", "mode": "ro"}})
|
||||
@ -334,8 +339,7 @@ class DockerAddon(DockerInterface):
|
||||
_LOGGER.warning("%s run with disabled protected mode!", self.addon.name)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -396,7 +400,7 @@ class DockerAddon(DockerInterface):
|
||||
# Update meta data
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -414,7 +418,7 @@ class DockerAddon(DockerInterface):
|
||||
"""
|
||||
try:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -423,7 +427,7 @@ class DockerAddon(DockerInterface):
|
||||
with tar_file.open("wb") as write_tar:
|
||||
for chunk in image:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -471,7 +475,7 @@ class DockerAddon(DockerInterface):
|
||||
# Load needed docker objects
|
||||
container = self.sys_docker.containers.get(self.name)
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
"""Audio docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
from ..const import ENV_TIME, MACHINE_ID
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -56,8 +54,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -69,7 +66,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
privileged=True,
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
environment={ENV_TIME: self.sys_config.timezone},
|
||||
volumes=self.volumes,
|
||||
)
|
||||
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""HA Cli docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from ..const import ENV_TIME, ENV_TOKEN
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -34,8 +32,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -50,7 +47,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
||||
detach=True,
|
||||
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
||||
environment={
|
||||
ENV_TIME: self.sys_timezone,
|
||||
ENV_TIME: self.sys_config.timezone,
|
||||
ENV_TOKEN: self.sys_plugins.cli.supervisor_token,
|
||||
},
|
||||
)
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""DNS docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from ..const import ENV_TIME
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -34,8 +32,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -47,7 +44,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
||||
name=self.name,
|
||||
hostname=self.name.replace("_", "-"),
|
||||
detach=True,
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
environment={ENV_TIME: self.sys_config.timezone},
|
||||
volumes={
|
||||
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"}
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
"""Init file for Supervisor Docker object."""
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from typing import Awaitable, Dict, Optional
|
||||
|
||||
import docker
|
||||
import requests
|
||||
|
||||
from ..const import ENV_TIME, ENV_TOKEN, ENV_TOKEN_OLD, LABEL_MACHINE, MACHINE_ID
|
||||
from ..exceptions import DockerAPIError
|
||||
@ -98,8 +98,7 @@ class DockerHomeAssistant(DockerInterface):
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -115,7 +114,7 @@ class DockerHomeAssistant(DockerInterface):
|
||||
environment={
|
||||
"HASSIO": 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_OLD: self.sys_homeassistant.supervisor_token,
|
||||
},
|
||||
@ -150,7 +149,7 @@ class DockerHomeAssistant(DockerInterface):
|
||||
"mode": "ro",
|
||||
},
|
||||
},
|
||||
environment={ENV_TIME: self.sys_timezone},
|
||||
environment={ENV_TIME: self.sys_config.timezone},
|
||||
)
|
||||
|
||||
def is_initialize(self) -> Awaitable[bool]:
|
||||
@ -167,8 +166,10 @@ class DockerHomeAssistant(DockerInterface):
|
||||
docker_image = self.sys_docker.images.get(
|
||||
f"{self.image}:{self.sys_homeassistant.version}"
|
||||
)
|
||||
except docker.errors.DockerException:
|
||||
except docker.errors.NotFound:
|
||||
return False
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
return DockerAPIError()
|
||||
|
||||
# we run on an old image, stop and start it
|
||||
if docker_container.image.id != docker_image.id:
|
||||
|
@ -5,6 +5,7 @@ import logging
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
|
||||
import docker
|
||||
import requests
|
||||
|
||||
from . import CommandReturn
|
||||
from ..const import LABEL_ARCH, LABEL_VERSION
|
||||
@ -107,6 +108,10 @@ class DockerInterface(CoreSysAttributes):
|
||||
free_space,
|
||||
)
|
||||
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:
|
||||
self._meta = docker_image.attrs
|
||||
|
||||
@ -119,7 +124,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
|
||||
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}")
|
||||
return True
|
||||
return False
|
||||
@ -138,8 +143,10 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
docker_container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except docker.errors.NotFound:
|
||||
return False
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
raise DockerAPIError()
|
||||
|
||||
return docker_container.status == "running"
|
||||
|
||||
@ -153,10 +160,10 @@ class DockerInterface(CoreSysAttributes):
|
||||
|
||||
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
|
||||
|
||||
with suppress(docker.errors.DockerException):
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
if not self._meta and self.image:
|
||||
self._meta = self.sys_docker.images.get(f"{self.image}:{tag}").attrs
|
||||
|
||||
@ -189,16 +196,18 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
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()
|
||||
|
||||
if docker_container.status == "running":
|
||||
_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)
|
||||
|
||||
if remove_container:
|
||||
with suppress(docker.errors.DockerException):
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
_LOGGER.info("Clean %s application", self.name)
|
||||
docker_container.remove(force=True)
|
||||
|
||||
@ -214,13 +223,14 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
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()
|
||||
|
||||
_LOGGER.info("Start %s", self.name)
|
||||
try:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -249,7 +259,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -296,12 +306,12 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
docker_container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
return b""
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
return b""
|
||||
@ -318,7 +328,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -327,7 +337,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
if origin.id == image.id:
|
||||
continue
|
||||
|
||||
with suppress(docker.errors.DockerException):
|
||||
with suppress(docker.errors.DockerException, requests.RequestException):
|
||||
_LOGGER.info("Cleanup images: %s", image.tags)
|
||||
self.sys_docker.images.remove(image.id, force=True)
|
||||
|
||||
@ -336,7 +346,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
return
|
||||
|
||||
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)
|
||||
self.sys_docker.images.remove(image.id, force=True)
|
||||
|
||||
@ -352,13 +362,13 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
raise DockerAPIError()
|
||||
|
||||
_LOGGER.info("Restart %s", self.image)
|
||||
try:
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -385,13 +395,13 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
docker_container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
raise DockerAPIError()
|
||||
|
||||
try:
|
||||
stats = docker_container.stats(stream=False)
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -409,7 +419,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
docker_container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
return False
|
||||
|
||||
# container is not running
|
||||
|
@ -1,10 +1,8 @@
|
||||
"""HA Cli docker object."""
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from ..const import ENV_TIME
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
from .interface import DockerInterface
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -34,8 +32,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
|
||||
return
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerAPIError):
|
||||
self._stop()
|
||||
self._stop()
|
||||
|
||||
# Create & Run container
|
||||
docker_container = self.sys_docker.run(
|
||||
@ -47,7 +44,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
|
||||
network_mode="host",
|
||||
detach=True,
|
||||
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
|
||||
|
@ -5,6 +5,7 @@ import logging
|
||||
from typing import List, Optional
|
||||
|
||||
import docker
|
||||
import requests
|
||||
|
||||
from ..const import DOCKER_NETWORK, DOCKER_NETWORK_MASK, DOCKER_NETWORK_RANGE
|
||||
from ..exceptions import DockerAPIError
|
||||
@ -35,9 +36,11 @@ class DockerNetwork:
|
||||
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)
|
||||
except docker.errors.NotFound:
|
||||
_LOGGER.warning("Docker network is corrupt! %s - run autofix", 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
|
||||
|
||||
@ -132,5 +135,5 @@ class DockerNetwork:
|
||||
|
||||
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)
|
||||
|
@ -5,6 +5,7 @@ import os
|
||||
from typing import Awaitable
|
||||
|
||||
import docker
|
||||
import requests
|
||||
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import DockerAPIError
|
||||
@ -38,7 +39,7 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
docker_container = self.sys_docker.containers.get(self.name)
|
||||
except docker.errors.DockerException:
|
||||
except (docker.errors.DockerException, requests.RequestException):
|
||||
raise DockerAPIError()
|
||||
|
||||
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="latest")
|
||||
except docker.errors.DockerException as err:
|
||||
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||
_LOGGER.error("Can't retag supervisor version: %s", err)
|
||||
raise DockerAPIError()
|
||||
|
||||
@ -101,6 +102,6 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
||||
continue
|
||||
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)
|
||||
raise DockerAPIError()
|
||||
|
@ -38,25 +38,40 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
if coresys.core.state in (CoreStates.INITIALIZE, CoreStates.SETUP):
|
||||
return event
|
||||
|
||||
# List installed addons
|
||||
installed_addons = [
|
||||
{"slug": addon.slug, "repository": addon.repository, "name": addon.name}
|
||||
for addon in coresys.addons.installed
|
||||
]
|
||||
|
||||
# Update information
|
||||
event.setdefault("extra", {}).update(
|
||||
event.setdefault("user", {}).update({"id": coresys.machine_id})
|
||||
event.setdefault("contexts", {}).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,
|
||||
"installed_addons": installed_addons,
|
||||
"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,
|
||||
"kernel": coresys.host.info.kernel,
|
||||
"core": coresys.homeassistant.version,
|
||||
"machine": coresys.machine,
|
||||
},
|
||||
"versions": {
|
||||
"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,
|
||||
}
|
||||
"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(
|
||||
@ -86,4 +101,5 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
|
||||
if key in [hdrs.HOST, hdrs.X_FORWARDED_HOST]:
|
||||
event["request"]["headers"][i] = [key, "example.com"]
|
||||
|
||||
return event
|
||||
|
@ -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+)")
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
@attr.s(slots=True, frozen=True)
|
||||
class Device:
|
||||
"""Represent a device."""
|
||||
|
||||
name: str = attr.ib()
|
||||
path: Path = attr.ib()
|
||||
subsystem: str = attr.ib()
|
||||
links: List[Path] = attr.ib()
|
||||
attributes: Dict[str, str] = attr.ib()
|
||||
|
||||
|
||||
class Hardware:
|
||||
@ -62,7 +64,9 @@ class Hardware:
|
||||
Device(
|
||||
device.sys_name,
|
||||
Path(device.device_node),
|
||||
device.subsystem,
|
||||
[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
|
||||
|
||||
@property
|
||||
def serial_devices(self) -> Set[str]:
|
||||
def serial_devices(self) -> List[Device]:
|
||||
"""Return all serial and connected devices."""
|
||||
dev_list: Set[str] = set()
|
||||
for device in self.context.list_devices(subsystem="tty"):
|
||||
if "ID_VENDOR" in device.properties or RE_TTY.search(device.device_node):
|
||||
dev_list.add(device.device_node)
|
||||
dev_list: List[Device] = []
|
||||
for device in self.devices:
|
||||
if device.subsystem != "tty" or (
|
||||
"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
|
||||
|
||||
@property
|
||||
def serial_by_id(self) -> Set[str]:
|
||||
"""Return all /dev/serial/by-id for serial devices."""
|
||||
dev_list: Set[str] = set()
|
||||
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
|
||||
def usb_devices(self) -> List[Device]:
|
||||
"""Return all usb and connected devices."""
|
||||
return [device for device in self.devices if device.subsystem == "usb"]
|
||||
|
||||
@property
|
||||
def input_devices(self) -> Set[str]:
|
||||
@ -115,12 +121,13 @@ class Hardware:
|
||||
return dev_list
|
||||
|
||||
@property
|
||||
def disk_devices(self) -> Set[str]:
|
||||
def disk_devices(self) -> List[Device]:
|
||||
"""Return all disk devices."""
|
||||
dev_list: Set[str] = set()
|
||||
for device in self.context.list_devices(subsystem="block"):
|
||||
if "ID_NAME" in device.properties:
|
||||
dev_list.add(device.device_node)
|
||||
dev_list: List[Device] = []
|
||||
for device in self.devices:
|
||||
if device.subsystem != "block" or "ID_NAME" not in device.attributes:
|
||||
continue
|
||||
dev_list.append(device)
|
||||
|
||||
return dev_list
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Common test functions."""
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
@ -39,6 +40,9 @@ async def coresys(loop, docker):
|
||||
coresys_obj.ingress.save_data = MagicMock()
|
||||
coresys_obj.arch._default_arch = "amd64"
|
||||
|
||||
coresys_obj._machine = "qemux86-64"
|
||||
coresys_obj._machine_id = uuid4()
|
||||
|
||||
yield coresys_obj
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Test sentry data filter."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreStates
|
||||
from supervisor.const import SUPERVISOR_VERSION, CoreStates
|
||||
from supervisor.exceptions import AddonConfigurationError
|
||||
from supervisor.misc.filter import filter_data
|
||||
|
||||
@ -58,7 +58,10 @@ def test_defaults(coresys):
|
||||
filtered = filter_data(coresys, SAMPLE_EVENT, {})
|
||||
|
||||
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):
|
||||
|
@ -16,10 +16,10 @@ def test_video_devices():
|
||||
"""Test video device filter."""
|
||||
system = Hardware()
|
||||
device_list = [
|
||||
Device("test-dev", Path("/dev/test-dev"), []),
|
||||
Device("vchiq", Path("/dev/vchiq"), []),
|
||||
Device("cec0", Path("/dev/cec0"), []),
|
||||
Device("video1", Path("/dev/video1"), []),
|
||||
Device("test-dev", Path("/dev/test-dev"), "xy", [], {}),
|
||||
Device("vchiq", Path("/dev/vchiq"), "xy", [], {}),
|
||||
Device("cec0", Path("/dev/cec0"), "xy", [], {}),
|
||||
Device("video1", Path("/dev/video1"), "xy", [], {}),
|
||||
]
|
||||
|
||||
with patch(
|
||||
@ -27,10 +27,80 @@ def test_video_devices():
|
||||
) as mock_device:
|
||||
mock_device.return_value = device_list
|
||||
|
||||
assert system.video_devices == [
|
||||
Device("vchiq", Path("/dev/vchiq"), []),
|
||||
Device("cec0", Path("/dev/cec0"), []),
|
||||
Device("video1", Path("/dev/video1"), []),
|
||||
assert [device.name for device in system.video_devices] == [
|
||||
"vchiq",
|
||||
"cec0",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user