1
mirror of https://github.com/home-assistant/supervisor synced 2024-09-07 16:20:07 +02:00

Add boards APIs (#3984)

* Add boards APIs

* Move boards to agent
This commit is contained in:
Mike Degatano 2022-11-04 03:22:24 -04:00 committed by GitHub
parent d59625e5b8
commit 672b220f69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 720 additions and 32 deletions

View File

@ -8,6 +8,7 @@ from aiohttp import web
from ..const import AddonState
from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.agent.boards.const import BOARD_NAME_SUPERVISED, BOARD_NAME_YELLOW
from ..exceptions import APIAddonNotInstalled
from .addons import APIAddons
from .audio import APIAudio
@ -179,6 +180,34 @@ class RestAPI(CoreSysAttributes):
]
)
# Boards endpoints
def get_board_routes(
board: str,
info_handler,
options_handler=None,
) -> list[web.RouteDef]:
routes = [
web.get(f"/os/boards/{board}", info_handler),
web.get(f"/os/boards/{board.lower()}", info_handler),
]
if options_handler:
routes.insert(1, web.post(f"/os/boards/{board}", options_handler))
routes.append(web.post(f"/os/boards/{board.lower()}", options_handler))
return routes
self.webapp.add_routes(
[
*get_board_routes(
BOARD_NAME_YELLOW,
api_os.boards_yellow_info,
api_os.boards_yellow_options,
),
*get_board_routes(BOARD_NAME_SUPERVISED, api_os.boards_supervised_info),
web.get("/os/boards/{board}", api_os.boards_other_info),
]
)
def _register_security(self) -> None:
"""Register Security functions."""
api_security = APISecurity()

View File

@ -21,14 +21,17 @@ ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
ATTR_BROADCAST_MDNS = "broadcast_mdns"
ATTR_DATA_DISK = "data_disk"
ATTR_DEVICE = "device"
ATTR_DISK_LED = "disk_led"
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
ATTR_DT_UTC = "dt_utc"
ATTR_FALLBACK = "fallback"
ATTR_HEARTBEAT_LED = "heartbeat_led"
ATTR_IDENTIFIERS = "identifiers"
ATTR_LLMNR = "llmnr"
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
ATTR_MDNS = "mdns"
ATTR_PANEL_PATH = "panel_path"
ATTR_POWER_LED = "power_led"
ATTR_SIGNED = "signed"
ATTR_STARTUP_TIME = "startup_time"
ATTR_UPDATE_TYPE = "update_type"

View File

@ -10,14 +10,23 @@ import voluptuous as vol
from ..const import (
ATTR_BOARD,
ATTR_BOOT,
ATTR_CPE_BOARD,
ATTR_DEVICES,
ATTR_UPDATE_AVAILABLE,
ATTR_VERSION,
ATTR_VERSION_LATEST,
)
from ..coresys import CoreSysAttributes
from ..exceptions import BoardInvalidError
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..validate import version_tag
from .const import ATTR_DATA_DISK, ATTR_DEVICE
from .const import (
ATTR_DATA_DISK,
ATTR_DEVICE,
ATTR_DISK_LED,
ATTR_HEARTBEAT_LED,
ATTR_POWER_LED,
)
from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -25,6 +34,15 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): vol.All(str, vol.Coerce(Path))})
# pylint: disable=no-value-for-parameter
SCHEMA_YELLOW_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_DISK_LED): vol.Boolean(),
vol.Optional(ATTR_HEARTBEAT_LED): vol.Boolean(),
vol.Optional(ATTR_POWER_LED): vol.Boolean(),
}
)
class APIOS(CoreSysAttributes):
"""Handle RESTful API for OS functions."""
@ -36,9 +54,10 @@ class APIOS(CoreSysAttributes):
ATTR_VERSION: self.sys_os.version,
ATTR_VERSION_LATEST: self.sys_os.latest_version,
ATTR_UPDATE_AVAILABLE: self.sys_os.need_update,
ATTR_BOARD: self.sys_os.board,
ATTR_BOARD: self.sys_dbus.agent.board.board,
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
ATTR_DATA_DISK: self.sys_os.datadisk.disk_used,
ATTR_CPE_BOARD: self.sys_os.board,
}
@api_process
@ -67,3 +86,49 @@ class APIOS(CoreSysAttributes):
return {
ATTR_DEVICES: self.sys_os.datadisk.available_disks,
}
@api_process
async def boards_yellow_info(self, request: web.Request) -> dict[str, Any]:
"""Get yellow board settings."""
return {
ATTR_DISK_LED: self.sys_dbus.agent.board.yellow.disk_led,
ATTR_HEARTBEAT_LED: self.sys_dbus.agent.board.yellow.heartbeat_led,
ATTR_POWER_LED: self.sys_dbus.agent.board.yellow.power_led,
}
@api_process
async def boards_yellow_options(self, request: web.Request) -> None:
"""Update yellow board settings."""
body = await api_validate(SCHEMA_YELLOW_OPTIONS, request)
if ATTR_DISK_LED in body:
self.sys_dbus.agent.board.yellow.disk_led = body[ATTR_DISK_LED]
if ATTR_HEARTBEAT_LED in body:
self.sys_dbus.agent.board.yellow.heartbeat_led = body[ATTR_HEARTBEAT_LED]
if ATTR_POWER_LED in body:
self.sys_dbus.agent.board.yellow.power_led = body[ATTR_POWER_LED]
self.sys_resolution.create_issue(
IssueType.REBOOT_REQUIRED,
ContextType.SYSTEM,
suggestions=[SuggestionType.EXECUTE_REBOOT],
)
@api_process
async def boards_supervised_info(self, request: web.Request) -> dict[str, Any]:
"""Get supervised board settings."""
# There are none currently, this rasises an error if a different board is in use
if self.sys_dbus.agent.board.supervised:
return {}
@api_process
async def boards_other_info(self, request: web.Request) -> dict[str, Any]:
"""Empty success return if board is in use, error otherwise."""
if request.match_info["board"] != self.sys_dbus.agent.board.board:
raise BoardInvalidError(
f"{request.match_info['board']} board is not in use", _LOGGER.error
)
return {}

View File

@ -128,6 +128,7 @@ ATTR_CONTAINERS = "containers"
ATTR_CONTENT = "content"
ATTR_CONTENT_TRUST = "content_trust"
ATTR_CPE = "cpe"
ATTR_CPE_BOARD = "cpe_board"
ATTR_CPU_PERCENT = "cpu_percent"
ATTR_CRYPTO = "crypto"
ATTR_DATA = "data"

View File

@ -14,9 +14,10 @@ from ..const import (
DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS,
)
from ..interface import DBusInterfaceProxy, dbus_property
from ..interface import DBusInterface, DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .apparmor import AppArmor
from .boards import BoardManager
from .cgroup import CGroup
from .datadisk import DataDisk
from .system import System
@ -36,10 +37,11 @@ class OSAgent(DBusInterfaceProxy):
"""Initialize Properties."""
self.properties: dict[str, Any] = {}
self._cgroup: CGroup = CGroup()
self._apparmor: AppArmor = AppArmor()
self._system: System = System()
self._board: BoardManager = BoardManager()
self._cgroup: CGroup = CGroup()
self._datadisk: DataDisk = DataDisk()
self._system: System = System()
@property
def cgroup(self) -> CGroup:
@ -61,6 +63,11 @@ class OSAgent(DBusInterfaceProxy):
"""Return DataDisk DBUS object."""
return self._datadisk
@property
def board(self) -> BoardManager:
"""Return board manager."""
return self._board
@property
@dbus_property
def version(self) -> AwesomeVersion:
@ -79,15 +86,17 @@ class OSAgent(DBusInterfaceProxy):
"""Enable or disable OS-Agent diagnostics."""
asyncio.create_task(self.dbus.set_diagnostics(value))
@property
def all(self) -> list[DBusInterface]:
"""Return all managed dbus interfaces."""
return [self.apparmor, self.board, self.cgroup, self.datadisk, self.system]
async def connect(self, bus: MessageBus) -> None:
"""Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
try:
await super().connect(bus)
await self.cgroup.connect(bus)
await self.apparmor.connect(bus)
await self.system.connect(bus)
await self.datadisk.connect(bus)
await asyncio.gather(*[dbus.connect(bus) for dbus in self.all])
except DBusError:
_LOGGER.warning("Can't connect to OS-Agent")
except DBusInterfaceError:
@ -100,19 +109,21 @@ class OSAgent(DBusInterfaceProxy):
"""Update Properties."""
await super().update(changed)
if not changed and self.apparmor.is_connected:
await self.apparmor.update()
if not changed and self.datadisk.is_connected:
await self.datadisk.update()
if not changed:
await asyncio.gather(
*[
dbus.update()
for dbus in [self.apparmor, self.board, self.datadisk]
if dbus.is_connected
]
)
def shutdown(self) -> None:
"""Shutdown the object and disconnect from D-Bus.
This method is irreversible.
"""
self.cgroup.shutdown()
self.apparmor.shutdown()
self.system.shutdown()
self.datadisk.shutdown()
for dbus in self.all:
dbus.shutdown()
super().shutdown()

View File

@ -0,0 +1,68 @@
"""Board management for OS Agent."""
import logging
from typing import Any
from dbus_fast.aio.message_bus import MessageBus
from ....exceptions import BoardInvalidError
from ...const import (
DBUS_ATTR_BOARD,
DBUS_IFACE_HAOS_BOARDS,
DBUS_NAME_HAOS,
DBUS_OBJECT_HAOS_BOARDS,
)
from ...interface import DBusInterfaceProxy, dbus_property
from .const import BOARD_NAME_SUPERVISED, BOARD_NAME_YELLOW
from .interface import BoardProxy
from .supervised import Supervised
from .yellow import Yellow
_LOGGER: logging.Logger = logging.getLogger(__name__)
class BoardManager(DBusInterfaceProxy):
"""Board manager object."""
bus_name: str = DBUS_NAME_HAOS
object_path: str = DBUS_OBJECT_HAOS_BOARDS
properties_interface: str = DBUS_IFACE_HAOS_BOARDS
sync_properties: bool = False
def __init__(self) -> None:
"""Initialize properties."""
self._board_proxy: BoardProxy | None = None
self.properties: dict[str, Any] = {}
@property
@dbus_property
def board(self) -> str:
"""Get board name."""
return self.properties[DBUS_ATTR_BOARD]
@property
def supervised(self) -> Supervised:
"""Get Supervised board."""
if self.board != BOARD_NAME_SUPERVISED:
raise BoardInvalidError("Supervised board is not in use", _LOGGER.error)
return self._board_proxy
@property
def yellow(self) -> Yellow:
"""Get Yellow board."""
if self.board != BOARD_NAME_YELLOW:
raise BoardInvalidError("Yellow board is not in use", _LOGGER.error)
return self._board_proxy
async def connect(self, bus: MessageBus) -> None:
"""Connect to D-Bus."""
await super().connect(bus)
if self.board == BOARD_NAME_YELLOW:
self._board_proxy = Yellow()
elif self.board == BOARD_NAME_SUPERVISED:
self._board_proxy = Supervised()
if self._board_proxy:
await self._board_proxy.connect(bus)

View File

@ -0,0 +1,4 @@
"""Constants for boards."""
BOARD_NAME_SUPERVISED = "Supervised"
BOARD_NAME_YELLOW = "Yellow"

View File

@ -0,0 +1,24 @@
"""Board dbus proxy interface."""
from typing import Any
from ...const import DBUS_IFACE_HAOS_BOARDS, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_BOARDS
from ...interface import DBusInterfaceProxy
class BoardProxy(DBusInterfaceProxy):
"""DBus interface proxy for os board."""
bus_name: str = DBUS_NAME_HAOS
def __init__(self, name: str) -> None:
"""Initialize properties."""
self._name: str = name
self.object_path: str = f"{DBUS_OBJECT_HAOS_BOARDS}/{name}"
self.properties_interface: str = f"{DBUS_IFACE_HAOS_BOARDS}.{name}"
self.properties: dict[str, Any] = {}
@property
def name(self) -> str:
"""Get name."""
return self._name

View File

@ -0,0 +1,13 @@
"""Supervised board management."""
from .const import BOARD_NAME_SUPERVISED
from .interface import BoardProxy
class Supervised(BoardProxy):
"""Supervised board manager object."""
def __init__(self) -> None:
"""Initialize properties."""
super().__init__(BOARD_NAME_SUPERVISED)
self.sync_properties: bool = False

View File

@ -0,0 +1,49 @@
"""Yellow board management."""
import asyncio
from ...const import DBUS_ATTR_DISK_LED, DBUS_ATTR_HEARTBEAT_LED, DBUS_ATTR_POWER_LED
from ...interface import dbus_property
from .const import BOARD_NAME_YELLOW
from .interface import BoardProxy
class Yellow(BoardProxy):
"""Yellow board manager object."""
def __init__(self) -> None:
"""Initialize properties."""
super().__init__(BOARD_NAME_YELLOW)
@property
@dbus_property
def heartbeat_led(self) -> bool:
"""Get heartbeat LED enabled."""
return self.properties[DBUS_ATTR_HEARTBEAT_LED]
@heartbeat_led.setter
def heartbeat_led(self, enabled: bool) -> None:
"""Enable/disable heartbeat LED."""
asyncio.create_task(self.dbus.Boards.Yellow.set_heartbeat_led(enabled))
@property
@dbus_property
def power_led(self) -> bool:
"""Get power LED enabled."""
return self.properties[DBUS_ATTR_POWER_LED]
@power_led.setter
def power_led(self, enabled: bool) -> None:
"""Enable/disable power LED."""
asyncio.create_task(self.dbus.Boards.Yellow.set_power_led(enabled))
@property
@dbus_property
def disk_led(self) -> bool:
"""Get disk LED enabled."""
return self.properties[DBUS_ATTR_DISK_LED]
@disk_led.setter
def disk_led(self, enabled: bool) -> None:
"""Enable/disable disk LED."""
asyncio.create_task(self.dbus.Boards.Yellow.set_disk_led(enabled))

View File

@ -18,6 +18,7 @@ DBUS_IFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"
DBUS_IFACE_DNS = "org.freedesktop.NetworkManager.DnsManager"
DBUS_IFACE_HAOS = "io.hass.os"
DBUS_IFACE_HAOS_APPARMOR = "io.hass.os.AppArmor"
DBUS_IFACE_HAOS_BOARDS = "io.hass.os.Boards"
DBUS_IFACE_HAOS_CGROUP = "io.hass.os.CGroup"
DBUS_IFACE_HAOS_DATADISK = "io.hass.os.DataDisk"
DBUS_IFACE_HAOS_SYSTEM = "io.hass.os.System"
@ -40,6 +41,7 @@ DBUS_OBJECT_BASE = "/"
DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager"
DBUS_OBJECT_HAOS = "/io/hass/os"
DBUS_OBJECT_HAOS_APPARMOR = "/io/hass/os/AppArmor"
DBUS_OBJECT_HAOS_BOARDS = "/io/hass/os/Boards"
DBUS_OBJECT_HAOS_CGROUP = "/io/hass/os/CGroup"
DBUS_OBJECT_HAOS_DATADISK = "/io/hass/os/DataDisk"
DBUS_OBJECT_HAOS_SYSTEM = "/io/hass/os/System"
@ -55,6 +57,7 @@ DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint"
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections"
DBUS_ATTR_ADDRESS_DATA = "AddressData"
DBUS_ATTR_BOARD = "Board"
DBUS_ATTR_BOOT_SLOT = "BootSlot"
DBUS_ATTR_CACHE_STATISTICS = "CacheStatistics"
DBUS_ATTR_CHASSIS = "Chassis"
@ -72,6 +75,7 @@ DBUS_ATTR_DEVICE_INTERFACE = "Interface"
DBUS_ATTR_DEVICE_TYPE = "DeviceType"
DBUS_ATTR_DEVICES = "Devices"
DBUS_ATTR_DIAGNOSTICS = "Diagnostics"
DBUS_ATTR_DISK_LED = "DiskLED"
DBUS_ATTR_DNS = "DNS"
DBUS_ATTR_DNS_EX = "DNSEx"
DBUS_ATTR_DNS_OVER_TLS = "DNSOverTLS"
@ -88,6 +92,7 @@ DBUS_ATTR_FINISH_TIMESTAMP = "FinishTimestamp"
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC = "FirmwareTimestampMonotonic"
DBUS_ATTR_FREQUENCY = "Frequency"
DBUS_ATTR_GATEWAY = "Gateway"
DBUS_ATTR_HEARTBEAT_LED = "HeartbeatLED"
DBUS_ATTR_HWADDRESS = "HwAddress"
DBUS_ATTR_ID = "Id"
DBUS_ATTR_IP4CONFIG = "Ip4Config"
@ -108,6 +113,7 @@ DBUS_ATTR_NTPSYNCHRONIZED = "NTPSynchronized"
DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME = "OperatingSystemPrettyName"
DBUS_ATTR_OPERATION = "Operation"
DBUS_ATTR_PARSER_VERSION = "ParserVersion"
DBUS_ATTR_POWER_LED = "PowerLED"
DBUS_ATTR_PRIMARY_CONNECTION = "PrimaryConnection"
DBUS_ATTR_RESOLV_CONF_MODE = "ResolvConfMode"
DBUS_ATTR_RCMANAGER = "RcManager"

View File

@ -88,13 +88,13 @@ class DBusManager(CoreSysAttributes):
"""Return all managed dbus interfaces."""
return [
self.agent,
self.systemd,
self.logind,
self.hostname,
self.timedate,
self.logind,
self.network,
self.rauc,
self.resolved,
self.systemd,
self.timedate,
]
async def load(self) -> None:

View File

@ -362,6 +362,13 @@ class AppArmorInvalidError(AppArmorError):
"""AppArmor profile validate error."""
# util/boards
class BoardInvalidError(DBusObjectError):
"""System does not use the board specified."""
# util/common

View File

@ -32,13 +32,13 @@ class UnsupportedReason(str, Enum):
"""Reasons for unsupported status."""
APPARMOR = "apparmor"
CGROUP_VERSION = "cgroup_version"
CONNECTIVITY_CHECK = "connectivity_check"
CONTENT_TRUST = "content_trust"
DBUS = "dbus"
DNS_SERVER = "dns_server"
DOCKER_CONFIGURATION = "docker_configuration"
DOCKER_VERSION = "docker_version"
CGROUP_VERSION = "cgroup_version"
JOB_CONDITIONS = "job_conditions"
LXC = "lxc"
NETWORK_MANAGER = "network_manager"
@ -79,6 +79,7 @@ class IssueType(str, Enum):
MISSING_IMAGE = "missing_image"
NO_CURRENT_BACKUP = "no_current_backup"
PWNED = "pwned"
REBOOT_REQUIRED = "reboot_required"
SECURITY = "security"
TRUST = "trust"
UPDATE_FAILED = "update_failed"
@ -90,11 +91,12 @@ class SuggestionType(str, Enum):
CLEAR_FULL_BACKUP = "clear_full_backup"
CREATE_FULL_BACKUP = "create_full_backup"
EXECUTE_UPDATE = "execute_update"
EXECUTE_REPAIR = "execute_repair"
EXECUTE_RESET = "execute_reset"
EXECUTE_INTEGRITY = "execute_integrity"
EXECUTE_REBOOT = "execute_reboot"
EXECUTE_RELOAD = "execute_reload"
EXECUTE_REMOVE = "execute_remove"
EXECUTE_REPAIR = "execute_repair"
EXECUTE_RESET = "execute_reset"
EXECUTE_STOP = "execute_stop"
EXECUTE_INTEGRITY = "execute_integrity"
EXECUTE_UPDATE = "execute_update"
REGISTRY_LOGIN = "registry_login"

View File

@ -0,0 +1,38 @@
"""Reboot host fixup."""
import asyncio
import logging
from ...coresys import CoreSys
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
def setup(coresys: CoreSys) -> FixupBase:
"""Check setup function."""
return FixupSystemExecuteReboot(coresys)
class FixupSystemExecuteReboot(FixupBase):
"""Storage class for fixup."""
async def process_fixup(self, reference: str | None = None) -> None:
"""Initialize the fixup class."""
_LOGGER.info("Rebooting the host")
await asyncio.shield(self.sys_host.control.reboot())
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
return SuggestionType.EXECUTE_REBOOT
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.REBOOT_REQUIRED]

View File

@ -1,16 +1,23 @@
"""Test OS API."""
from pathlib import Path
import asyncio
from pathlib import Path
from unittest.mock import PropertyMock, patch
from aiohttp.test_utils import TestClient
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.agent.boards import BoardManager
from supervisor.hardware.data import Device
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion
# pylint: disable=protected-access
@pytest.mark.asyncio
async def test_api_os_info(api_client):
async def test_api_os_info(api_client: TestClient):
"""Test docker info api."""
resp = await api_client.get("/os/info")
result = await resp.json()
@ -22,12 +29,13 @@ async def test_api_os_info(api_client):
"board",
"boot",
"data_disk",
"cpe_board",
):
assert attr in result["data"]
@pytest.mark.asyncio
async def test_api_os_info_with_agent(api_client, coresys: CoreSys):
async def test_api_os_info_with_agent(api_client: TestClient, coresys: CoreSys):
"""Test docker info api."""
await coresys.dbus.agent.connect(coresys.dbus.bus)
await coresys.dbus.agent.update()
@ -39,7 +47,7 @@ async def test_api_os_info_with_agent(api_client, coresys: CoreSys):
@pytest.mark.asyncio
async def test_api_os_datadisk_move(api_client, coresys: CoreSys):
async def test_api_os_datadisk_move(api_client: TestClient, coresys: CoreSys):
"""Test datadisk move without exists disk."""
await coresys.dbus.agent.connect(coresys.dbus.bus)
await coresys.dbus.agent.update()
@ -52,7 +60,7 @@ async def test_api_os_datadisk_move(api_client, coresys: CoreSys):
@pytest.mark.asyncio
async def test_api_os_datadisk_list(api_client, coresys: CoreSys):
async def test_api_os_datadisk_list(api_client: TestClient, coresys: CoreSys):
"""Test datadisk list function."""
await coresys.dbus.agent.connect(coresys.dbus.bus)
await coresys.dbus.agent.update()
@ -86,3 +94,81 @@ async def test_api_os_datadisk_list(api_client, coresys: CoreSys):
result = await resp.json()
assert result["data"]["devices"] == ["/dev/sda"]
@pytest.mark.parametrize("name", ["Yellow", "yellow"])
async def test_api_board_yellow_info(
api_client: TestClient, coresys: CoreSys, name: str
):
"""Test yellow board info."""
await coresys.dbus.agent.board.connect(coresys.dbus.bus)
resp = await api_client.get(f"/os/boards/{name}")
assert resp.status == 200
result = await resp.json()
assert result["data"]["disk_led"] is True
assert result["data"]["heartbeat_led"] is True
assert result["data"]["power_led"] is True
assert (await api_client.get("/os/boards/supervised")).status == 400
assert (await api_client.get("/os/boards/NotReal")).status == 400
@pytest.mark.parametrize("name", ["Yellow", "yellow"])
async def test_api_board_yellow_options(
api_client: TestClient, coresys: CoreSys, dbus: list[str], name: str
):
"""Test yellow board options."""
await coresys.dbus.agent.board.connect(coresys.dbus.bus)
assert len(coresys.resolution.issues) == 0
dbus.clear()
resp = await api_client.post(
f"/os/boards/{name}",
json={"disk_led": False, "heartbeat_led": False, "power_led": False},
)
assert resp.status == 200
await asyncio.sleep(0)
assert dbus == [
"/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.DiskLED",
"/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.HeartbeatLED",
"/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.PowerLED",
]
assert (
Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
in coresys.resolution.issues
)
assert (
Suggestion(SuggestionType.EXECUTE_REBOOT, ContextType.SYSTEM)
in coresys.resolution.suggestions
)
@pytest.mark.parametrize("name", ["Supervised", "supervised"])
async def test_api_board_supervised_info(
api_client: TestClient, coresys: CoreSys, name: str
):
"""Test supervised board info."""
with patch.object(
BoardManager, "board", new=PropertyMock(return_value="Supervised")
):
await coresys.dbus.agent.board.connect(coresys.dbus.bus)
assert (await api_client.get(f"/os/boards/{name}")).status == 200
assert (await api_client.post(f"/os/boards/{name}", json={})).status == 405
assert (await api_client.get("/os/boards/yellow")).status == 400
assert (await api_client.get("/os/boards/NotReal")).status == 400
async def test_api_board_other_info(api_client: TestClient, coresys: CoreSys):
"""Test info for other board without dbus object."""
with patch.object(BoardManager, "board", new=PropertyMock(return_value="NotReal")):
await coresys.dbus.agent.board.connect(coresys.dbus.bus)
assert (await api_client.get("/os/boards/NotReal")).status == 200
assert (await api_client.post("/os/boards/NotReal", json={})).status == 405
assert (await api_client.get("/os/boards/yellow")).status == 400
assert (await api_client.get("/os/boards/supervised")).status == 400

View File

@ -0,0 +1 @@
"""Test for Boards D-Bus interfaces."""

View File

@ -0,0 +1,78 @@
"""Test Boards manager."""
from unittest.mock import patch
from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.aio.proxy_object import ProxyInterface
import pytest
from supervisor.dbus.agent.boards import BoardManager
from supervisor.exceptions import BoardInvalidError
from supervisor.utils.dbus import DBUS_INTERFACE_PROPERTIES, DBus
@pytest.fixture(name="dbus_mock_board")
async def fixture_dbus_mock_board(request: pytest.FixtureRequest, dbus: list[str]):
"""Mock Boards dbus object to particular board name for tests."""
call_dbus = DBus.call_dbus
async def mock_call_dbus_specify_board(
proxy_interface: ProxyInterface,
method: str,
*args,
unpack_variants: bool = True,
):
if (
proxy_interface.introspection.name == DBUS_INTERFACE_PROPERTIES
and method == "call_get_all"
and proxy_interface.path == "/io/hass/os/Boards"
):
return {"Board": request.param}
return call_dbus(
proxy_interface, method, *args, unpack_variants=unpack_variants
)
with patch(
"supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus_specify_board
):
yield dbus
async def test_dbus_board(dbus: list[str], dbus_bus: MessageBus):
"""Test DBus Board load."""
board = BoardManager()
await board.connect(dbus_bus)
assert board.board == "Yellow"
assert board.yellow.power_led is True
with pytest.raises(BoardInvalidError):
assert not board.supervised
@pytest.mark.parametrize("dbus_mock_board", ["Supervised"], indirect=True)
async def test_dbus_board_supervised(dbus_mock_board: list[str], dbus_bus: MessageBus):
"""Test DBus Board load with supervised board."""
board = BoardManager()
await board.connect(dbus_bus)
assert board.board == "Supervised"
assert board.supervised
with pytest.raises(BoardInvalidError):
assert not board.yellow
@pytest.mark.parametrize("dbus_mock_board", ["NotReal"], indirect=True)
async def test_dbus_board_other(dbus_mock_board: list[str], dbus_bus: MessageBus):
"""Test DBus Board load with board that has no dbus object."""
board = BoardManager()
await board.connect(dbus_bus)
assert board.board == "NotReal"
with pytest.raises(BoardInvalidError):
assert not board.yellow
with pytest.raises(BoardInvalidError):
assert not board.supervised

View File

@ -0,0 +1,54 @@
"""Test Yellow board."""
import asyncio
from dbus_fast.aio.message_bus import MessageBus
from supervisor.dbus.agent.boards.yellow import Yellow
async def test_dbus_yellow(dbus: list[str], dbus_bus: MessageBus):
"""Test Yellow board load."""
yellow = Yellow()
await yellow.connect(dbus_bus)
assert yellow.name == "Yellow"
assert yellow.disk_led is True
assert yellow.heartbeat_led is True
assert yellow.power_led is True
async def test_dbus_yellow_set_disk_led(dbus: list[str], dbus_bus: MessageBus):
"""Test setting disk led for Yellow board."""
yellow = Yellow()
await yellow.connect(dbus_bus)
dbus.clear()
yellow.disk_led = False
await asyncio.sleep(0)
assert dbus == ["/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.DiskLED"]
async def test_dbus_yellow_set_heartbeat_led(dbus: list[str], dbus_bus: MessageBus):
"""Test setting heartbeat led for Yellow board."""
yellow = Yellow()
await yellow.connect(dbus_bus)
dbus.clear()
yellow.heartbeat_led = False
await asyncio.sleep(0)
assert dbus == ["/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.HeartbeatLED"]
async def test_dbus_yellow_set_power_led(dbus: list[str], dbus_bus: MessageBus):
"""Test setting power led for Yellow board."""
yellow = Yellow()
await yellow.connect(dbus_bus)
dbus.clear()
yellow.power_led = False
await asyncio.sleep(0)
assert dbus == ["/io/hass/os/Boards/Yellow-io.hass.os.Boards.Yellow.PowerLED"]

View File

@ -23,7 +23,7 @@ async def test_dbus_osagent_apparmor(coresys: CoreSys):
await asyncio.sleep(0)
assert coresys.dbus.agent.apparmor.version == "1.0.0"
fire_property_change_signal(coresys.dbus.agent, {}, ["ParserVersion"])
fire_property_change_signal(coresys.dbus.agent.apparmor, {}, ["ParserVersion"])
await asyncio.sleep(0)
assert coresys.dbus.agent.apparmor.version == "2.13.2"

3
tests/fixtures/io_hass_os_Boards.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"Board": "Yellow"
}

35
tests/fixtures/io_hass_os_Boards.xml vendored Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/io/hass/os/Boards">
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="out" type="s" direction="out"></arg>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="out"></arg>
</method>
<method name="GetAll">
<arg name="interface" type="s" direction="in"></arg>
<arg name="props" type="a{sv}" direction="out"></arg>
</method>
<method name="Set">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="in"></arg>
</method>
<signal name="PropertiesChanged">
<arg name="interface" type="s" direction="out"></arg>
<arg name="changed_properties" type="a{sv}" direction="out"></arg>
<arg name="invalidates_properties" type="as" direction="out"></arg>
</signal>
</interface>
<interface name="io.hass.os.Boards">
<property name="Board" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"></annotation>
</property>
</interface>
</node>

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,31 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/io/hass/os/Boards/Supervised">
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="out" type="s" direction="out"></arg>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="out"></arg>
</method>
<method name="GetAll">
<arg name="interface" type="s" direction="in"></arg>
<arg name="props" type="a{sv}" direction="out"></arg>
</method>
<method name="Set">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="in"></arg>
</method>
<signal name="PropertiesChanged">
<arg name="interface" type="s" direction="out"></arg>
<arg name="changed_properties" type="a{sv}" direction="out"></arg>
<arg name="invalidates_properties" type="as" direction="out"></arg>
</signal>
</interface>
<interface name="io.hass.os.Boards.Supervised"></interface>
</node>

View File

@ -0,0 +1,5 @@
{
"HeartbeatLED": true,
"PowerLED": true,
"DiskLED": true
}

View File

@ -0,0 +1,41 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/io/hass/os/Boards/Yellow">
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="out" type="s" direction="out"></arg>
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="out"></arg>
</method>
<method name="GetAll">
<arg name="interface" type="s" direction="in"></arg>
<arg name="props" type="a{sv}" direction="out"></arg>
</method>
<method name="Set">
<arg name="interface" type="s" direction="in"></arg>
<arg name="property" type="s" direction="in"></arg>
<arg name="value" type="v" direction="in"></arg>
</method>
<signal name="PropertiesChanged">
<arg name="interface" type="s" direction="out"></arg>
<arg name="changed_properties" type="a{sv}" direction="out"></arg>
<arg name="invalidates_properties" type="as" direction="out"></arg>
</signal>
</interface>
<interface name="io.hass.os.Boards.Yellow">
<property name="HeartbeatLED" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"></annotation>
</property>
<property name="PowerLED" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"></annotation>
</property>
<property name="DiskLED" type="b" access="readwrite">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"></annotation>
</property>
</interface>
</node>

View File

@ -0,0 +1,33 @@
"""Test fixup system reboot."""
from unittest.mock import PropertyMock, patch
from supervisor.coresys import CoreSys
from supervisor.host.const import HostFeature
from supervisor.host.manager import HostManager
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion
from supervisor.resolution.fixups.system_execute_reboot import FixupSystemExecuteReboot
async def test_fixup(coresys: CoreSys, dbus: list[str]):
"""Test fixup."""
await coresys.dbus.logind.connect(coresys.dbus.bus)
dbus.clear()
system_execute_reboot = FixupSystemExecuteReboot(coresys)
assert system_execute_reboot.auto is False
coresys.resolution.suggestions = Suggestion(
SuggestionType.EXECUTE_REBOOT, ContextType.SYSTEM
)
coresys.resolution.issues = Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
with patch.object(
HostManager, "features", new=PropertyMock(return_value=[HostFeature.REBOOT])
):
await system_execute_reboot()
assert dbus == ["/org/freedesktop/login1-org.freedesktop.login1.Manager.Reboot"]
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0