Addon provide his own udev support (#1206)
* Addon provide his own udev support * upgrade logger
This commit is contained in:
parent
41ce9913d2
commit
0a0a62f238
1
API.md
1
API.md
|
@ -502,6 +502,7 @@ Get all available addons.
|
|||
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
|
||||
"apparmor": "disable|default|profile",
|
||||
"devices": ["/dev/xy"],
|
||||
"udev": "bool",
|
||||
"auto_uart": "bool",
|
||||
"icon": "bool",
|
||||
"logo": "bool",
|
||||
|
|
|
@ -51,6 +51,7 @@ from ..const import (
|
|||
ATTR_STDIN,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
ATTR_WEBUI,
|
||||
|
@ -343,6 +344,11 @@ class AddonModel(CoreSysAttributes):
|
|||
"""Return True if the add-on access to GPIO interface."""
|
||||
return self.data[ATTR_GPIO]
|
||||
|
||||
@property
|
||||
def with_udev(self) -> bool:
|
||||
"""Return True if the add-on have his own udev."""
|
||||
return self.data[ATTR_UDEV]
|
||||
|
||||
@property
|
||||
def with_kernel_modules(self) -> bool:
|
||||
"""Return True if the add-on access to kernel modules."""
|
||||
|
|
|
@ -68,6 +68,7 @@ from ..const import (
|
|||
ATTR_SYSTEM,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
|
@ -186,6 +187,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
||||
vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
|
||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
||||
|
|
|
@ -8,6 +8,7 @@ import voluptuous as vol
|
|||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from ..addons import AnyAddon
|
||||
from ..docker.stats import DockerStats
|
||||
from ..addons.utils import rating_security
|
||||
from ..const import (
|
||||
ATTR_ADDONS,
|
||||
|
@ -58,8 +59,8 @@ from ..const import (
|
|||
ATTR_MACHINE,
|
||||
ATTR_MAINTAINER,
|
||||
ATTR_MEMORY_LIMIT,
|
||||
ATTR_MEMORY_USAGE,
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_MEMORY_USAGE,
|
||||
ATTR_NAME,
|
||||
ATTR_NETWORK,
|
||||
ATTR_NETWORK_DESCRIPTION,
|
||||
|
@ -76,6 +77,7 @@ from ..const import (
|
|||
ATTR_SOURCE,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
ATTR_WEBUI,
|
||||
|
@ -119,7 +121,7 @@ class APIAddons(CoreSysAttributes):
|
|||
self, request: web.Request, check_installed: bool = True
|
||||
) -> AnyAddon:
|
||||
"""Return addon, throw an exception it it doesn't exist."""
|
||||
addon_slug = request.match_info.get("addon")
|
||||
addon_slug: str = request.match_info.get("addon")
|
||||
|
||||
# Lookup itself
|
||||
if addon_slug == "self":
|
||||
|
@ -178,7 +180,7 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return add-on information."""
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
||||
|
||||
data = {
|
||||
ATTR_NAME: addon.name,
|
||||
|
@ -225,6 +227,7 @@ class APIAddons(CoreSysAttributes):
|
|||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
ATTR_DOCKER_API: addon.access_docker_api,
|
||||
ATTR_AUDIO: addon.with_audio,
|
||||
ATTR_AUDIO_INPUT: None,
|
||||
|
@ -261,12 +264,12 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def options(self, request: web.Request) -> None:
|
||||
"""Store user options for add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
|
||||
addon_schema = SCHEMA_OPTIONS.extend(
|
||||
{vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)}
|
||||
)
|
||||
body = await api_validate(addon_schema, request)
|
||||
body: Dict[str, Any] = await api_validate(addon_schema, request)
|
||||
|
||||
if ATTR_OPTIONS in body:
|
||||
addon.options = body[ATTR_OPTIONS]
|
||||
|
@ -289,8 +292,8 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def security(self, request: web.Request) -> None:
|
||||
"""Store security options for add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
body = await api_validate(SCHEMA_SECURITY, request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||
|
||||
if ATTR_PROTECTED in body:
|
||||
_LOGGER.warning("Protected flag changing for %s!", addon.slug)
|
||||
|
@ -301,8 +304,8 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return resource information."""
|
||||
addon = self._extract_addon(request)
|
||||
stats = await addon.stats()
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
stats: DockerStats = await addon.stats()
|
||||
|
||||
return {
|
||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||
|
@ -318,19 +321,19 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
def install(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Install add-on."""
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
||||
return asyncio.shield(addon.install())
|
||||
|
||||
@api_process
|
||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Uninstall add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.uninstall())
|
||||
|
||||
@api_process
|
||||
def start(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Start add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
|
||||
# check options
|
||||
options = addon.options
|
||||
|
@ -344,13 +347,13 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Stop add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.stop())
|
||||
|
||||
@api_process
|
||||
def update(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Update add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
|
||||
if addon.latest_version == addon.version:
|
||||
raise APIError("No update available!")
|
||||
|
@ -360,13 +363,13 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Restart add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.restart())
|
||||
|
||||
@api_process
|
||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Rebuild local build add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
if not addon.need_build:
|
||||
raise APIError("Only local build addons are supported")
|
||||
|
||||
|
@ -375,13 +378,13 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||
"""Return logs from add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
return addon.logs()
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_PNG)
|
||||
async def icon(self, request: web.Request) -> bytes:
|
||||
"""Return icon from add-on."""
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
||||
if not addon.with_icon:
|
||||
raise APIError("No icon found!")
|
||||
|
||||
|
@ -391,7 +394,7 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process_raw(CONTENT_TYPE_PNG)
|
||||
async def logo(self, request: web.Request) -> bytes:
|
||||
"""Return logo from add-on."""
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
||||
if not addon.with_logo:
|
||||
raise APIError("No logo found!")
|
||||
|
||||
|
@ -401,7 +404,7 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||
async def changelog(self, request: web.Request) -> str:
|
||||
"""Return changelog from add-on."""
|
||||
addon = self._extract_addon(request, check_installed=False)
|
||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
||||
if not addon.with_changelog:
|
||||
raise APIError("No changelog found!")
|
||||
|
||||
|
@ -411,7 +414,7 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def stdin(self, request: web.Request) -> None:
|
||||
"""Write to stdin of add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
if not addon.with_stdin:
|
||||
raise APIError("STDIN not supported by add-on")
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ class APIHardware(CoreSysAttributes):
|
|||
async def info(self, request):
|
||||
"""Show hardware info."""
|
||||
return {
|
||||
ATTR_SERIAL: list(self.sys_hardware.serial_devices),
|
||||
ATTR_SERIAL: list(
|
||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
||||
),
|
||||
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
||||
ATTR_DISK: list(self.sys_hardware.disk_devices),
|
||||
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
||||
|
|
|
@ -218,6 +218,7 @@ ATTR_DEBUG = "debug"
|
|||
ATTR_DEBUG_BLOCK = "debug_block"
|
||||
ATTR_DNS = "dns"
|
||||
ATTR_SERVERS = "servers"
|
||||
ATTR_UDEV = "udev"
|
||||
|
||||
PROVIDE_SERVICE = "provide"
|
||||
NEED_SERVICE = "need"
|
||||
|
|
|
@ -135,7 +135,14 @@ class DockerAddon(DockerInterface):
|
|||
|
||||
# Auto mapping UART devices
|
||||
if self.addon.auto_uart:
|
||||
for device in self.sys_hardware.serial_devices:
|
||||
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")
|
||||
|
||||
# Return None if no devices is present
|
||||
|
|
|
@ -3,25 +3,26 @@ from datetime import datetime
|
|||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
import pyudev
|
||||
|
||||
from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES, CHAN_ID, CHAN_TYPE
|
||||
from ..const import ATTR_DEVICES, ATTR_NAME, ATTR_TYPE, CHAN_ID, CHAN_TYPE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ASOUND_CARDS = Path("/proc/asound/cards")
|
||||
RE_CARDS = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)")
|
||||
ASOUND_CARDS: Path = Path("/proc/asound/cards")
|
||||
RE_CARDS: re.Pattern = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)")
|
||||
|
||||
ASOUND_DEVICES = Path("/proc/asound/devices")
|
||||
RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
|
||||
ASOUND_DEVICES: Path = Path("/proc/asound/devices")
|
||||
RE_DEVICES: re.Pattern = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
|
||||
|
||||
PROC_STAT = Path("/proc/stat")
|
||||
RE_BOOT_TIME = re.compile(r"btime (\d+)")
|
||||
PROC_STAT: Path = Path("/proc/stat")
|
||||
RE_BOOT_TIME: re.Pattern = re.compile(r"btime (\d+)")
|
||||
|
||||
GPIO_DEVICES = Path("/sys/class/gpio")
|
||||
SOC_DEVICES = Path("/sys/devices/platform/soc")
|
||||
RE_TTY = re.compile(r"tty[A-Z]+")
|
||||
GPIO_DEVICES: Path = Path("/sys/class/gpio")
|
||||
SOC_DEVICES: Path = Path("/sys/devices/platform/soc")
|
||||
RE_TTY: re.Pattern = re.compile(r"tty[A-Z]+")
|
||||
|
||||
|
||||
class Hardware:
|
||||
|
@ -32,13 +33,21 @@ class Hardware:
|
|||
self.context = pyudev.Context()
|
||||
|
||||
@property
|
||||
def serial_devices(self):
|
||||
def serial_devices(self) -> Set[str]:
|
||||
"""Return all serial and connected devices."""
|
||||
dev_list = set()
|
||||
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)
|
||||
|
||||
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"):
|
||||
|
@ -48,9 +57,9 @@ class Hardware:
|
|||
return dev_list
|
||||
|
||||
@property
|
||||
def input_devices(self):
|
||||
def input_devices(self) -> Set[str]:
|
||||
"""Return all input devices."""
|
||||
dev_list = set()
|
||||
dev_list: Set[str] = set()
|
||||
for device in self.context.list_devices(subsystem="input"):
|
||||
if "NAME" in device.properties:
|
||||
dev_list.add(device.properties["NAME"].replace('"', ""))
|
||||
|
@ -58,9 +67,9 @@ class Hardware:
|
|||
return dev_list
|
||||
|
||||
@property
|
||||
def disk_devices(self):
|
||||
def disk_devices(self) -> Set[str]:
|
||||
"""Return all disk devices."""
|
||||
dev_list = set()
|
||||
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)
|
||||
|
@ -68,15 +77,15 @@ class Hardware:
|
|||
return dev_list
|
||||
|
||||
@property
|
||||
def support_audio(self):
|
||||
def support_audio(self) -> bool:
|
||||
"""Return True if the system have audio support."""
|
||||
return bool(self.audio_devices)
|
||||
|
||||
@property
|
||||
def audio_devices(self):
|
||||
def audio_devices(self) -> Dict[str, Any]:
|
||||
"""Return all available audio interfaces."""
|
||||
if not ASOUND_CARDS.exists():
|
||||
_LOGGER.debug("No audio devices found")
|
||||
_LOGGER.info("No audio devices found")
|
||||
return {}
|
||||
|
||||
try:
|
||||
|
@ -86,7 +95,7 @@ class Hardware:
|
|||
_LOGGER.error("Can't read asound data: %s", err)
|
||||
return {}
|
||||
|
||||
audio_list = {}
|
||||
audio_list: Dict[str, Any] = {}
|
||||
|
||||
# parse cards
|
||||
for match in RE_CARDS.finditer(cards):
|
||||
|
@ -109,31 +118,31 @@ class Hardware:
|
|||
return audio_list
|
||||
|
||||
@property
|
||||
def support_gpio(self):
|
||||
def support_gpio(self) -> bool:
|
||||
"""Return True if device support GPIOs."""
|
||||
return SOC_DEVICES.exists() and GPIO_DEVICES.exists()
|
||||
|
||||
@property
|
||||
def gpio_devices(self):
|
||||
def gpio_devices(self) -> Set[str]:
|
||||
"""Return list of GPIO interface on device."""
|
||||
dev_list = set()
|
||||
dev_list: Set[str] = set()
|
||||
for interface in GPIO_DEVICES.glob("gpio*"):
|
||||
dev_list.add(interface.name)
|
||||
|
||||
return dev_list
|
||||
|
||||
@property
|
||||
def last_boot(self):
|
||||
def last_boot(self) -> Optional[str]:
|
||||
"""Return last boot time."""
|
||||
try:
|
||||
with PROC_STAT.open("r") as stat_file:
|
||||
stats = stat_file.read()
|
||||
stats: str = stat_file.read()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't read stat data: %s", err)
|
||||
return None
|
||||
|
||||
# parse stat file
|
||||
found = RE_BOOT_TIME.search(stats)
|
||||
found: Optional[re.Match] = RE_BOOT_TIME.search(stats)
|
||||
if not found:
|
||||
_LOGGER.error("Can't found last boot time!")
|
||||
return None
|
||||
|
|
Loading…
Reference in New Issue