mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
commit
e9802f92c9
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -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
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",
|
"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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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,8 +145,11 @@ 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
|
||||||
if await self.sys_run_in_executor(self.sys_docker.check_denylist_images):
|
try:
|
||||||
self.coresys.supported = False
|
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
|
self.healthy = False
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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,14 +237,19 @@ 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()
|
||||||
for image in self.images.list():
|
|
||||||
for tag in image.tags:
|
try:
|
||||||
image_name = tag.split(":")[0]
|
for image in self.images.list():
|
||||||
if (
|
for tag in image.tags:
|
||||||
image_name in DOCKER_IMAGE_DENYLIST
|
image_name = tag.split(":")[0]
|
||||||
and image_name not in denied_images
|
if (
|
||||||
):
|
image_name in DOCKER_IMAGE_DENYLIST
|
||||||
denied_images.add(image_name)
|
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:
|
if not denied_images:
|
||||||
return False
|
return False
|
||||||
|
@ -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,20 +127,21 @@ 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:
|
||||||
if self.addon.with_udev:
|
for device in self.sys_hardware.serial_devices:
|
||||||
serial_devs = self.sys_hardware.serial_devices
|
devices.append(f"{device.path.as_posix()}:{device.path.as_posix()}:rwm")
|
||||||
else:
|
if self.addon.with_udev:
|
||||||
serial_devs = (
|
continue
|
||||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
for device_link in device.links:
|
||||||
)
|
devices.append(
|
||||||
|
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:
|
||||||
@ -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,8 +339,7 @@ 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
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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()
|
||||||
|
|
||||||
|
@ -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,8 +54,7 @@ class DockerAudio(DockerInterface, CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
self._stop()
|
||||||
self._stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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,8 +32,7 @@ class DockerCli(DockerInterface, CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
self._stop()
|
||||||
self._stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -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,8 +32,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
self._stop()
|
||||||
self._stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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"}
|
||||||
},
|
},
|
||||||
|
@ -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,8 +98,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
self._stop()
|
||||||
self._stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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,8 +32,7 @@ class DockerMulticast(DockerInterface, CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
self._stop()
|
||||||
self._stop()
|
|
||||||
|
|
||||||
# Create & Run container
|
# Create & Run container
|
||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user