mirror of
https://github.com/home-assistant/supervisor
synced 2024-09-07 16:20:07 +02:00
parent
d59625e5b8
commit
672b220f69
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
68
supervisor/dbus/agent/boards/__init__.py
Normal file
68
supervisor/dbus/agent/boards/__init__.py
Normal 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)
|
4
supervisor/dbus/agent/boards/const.py
Normal file
4
supervisor/dbus/agent/boards/const.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Constants for boards."""
|
||||
|
||||
BOARD_NAME_SUPERVISED = "Supervised"
|
||||
BOARD_NAME_YELLOW = "Yellow"
|
24
supervisor/dbus/agent/boards/interface.py
Normal file
24
supervisor/dbus/agent/boards/interface.py
Normal 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
|
13
supervisor/dbus/agent/boards/supervised.py
Normal file
13
supervisor/dbus/agent/boards/supervised.py
Normal 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
|
49
supervisor/dbus/agent/boards/yellow.py
Normal file
49
supervisor/dbus/agent/boards/yellow.py
Normal 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))
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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"
|
||||
|
38
supervisor/resolution/fixups/system_execute_reboot.py
Normal file
38
supervisor/resolution/fixups/system_execute_reboot.py
Normal 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]
|
@ -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
|
||||
|
1
tests/dbus/agent/boards/__init__.py
Normal file
1
tests/dbus/agent/boards/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Test for Boards D-Bus interfaces."""
|
78
tests/dbus/agent/boards/test_board.py
Normal file
78
tests/dbus/agent/boards/test_board.py
Normal 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
|
54
tests/dbus/agent/boards/test_yellow.py
Normal file
54
tests/dbus/agent/boards/test_yellow.py
Normal 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"]
|
@ -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
3
tests/fixtures/io_hass_os_Boards.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"Board": "Yellow"
|
||||
}
|
35
tests/fixtures/io_hass_os_Boards.xml
vendored
Normal file
35
tests/fixtures/io_hass_os_Boards.xml
vendored
Normal 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>
|
1
tests/fixtures/io_hass_os_Boards_Supervised.json
vendored
Normal file
1
tests/fixtures/io_hass_os_Boards_Supervised.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{}
|
31
tests/fixtures/io_hass_os_Boards_Supervised.xml
vendored
Normal file
31
tests/fixtures/io_hass_os_Boards_Supervised.xml
vendored
Normal 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>
|
5
tests/fixtures/io_hass_os_Boards_Yellow.json
vendored
Normal file
5
tests/fixtures/io_hass_os_Boards_Yellow.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"HeartbeatLED": true,
|
||||
"PowerLED": true,
|
||||
"DiskLED": true
|
||||
}
|
41
tests/fixtures/io_hass_os_Boards_Yellow.xml
vendored
Normal file
41
tests/fixtures/io_hass_os_Boards_Yellow.xml
vendored
Normal 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>
|
33
tests/resolution/fixup/test_system_execute_reboot.py
Normal file
33
tests/resolution/fixup/test_system_execute_reboot.py
Normal 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
|
Loading…
Reference in New Issue
Block a user