mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-23 17:57:04 +00:00
Compare commits
1 Commits
main
...
handle-sup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbb4eab381 |
13
build.yaml
13
build.yaml
@@ -1,10 +1,13 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.13-alpine3.22-2025.11.1
|
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.13-alpine3.22
|
||||||
armhf: ghcr.io/home-assistant/armhf-base-python:3.13-alpine3.22-2025.11.1
|
armhf: ghcr.io/home-assistant/armhf-base-python:3.13-alpine3.22
|
||||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.13-alpine3.22-2025.11.1
|
armv7: ghcr.io/home-assistant/armv7-base-python:3.13-alpine3.22
|
||||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.13-alpine3.22-2025.11.1
|
amd64: ghcr.io/home-assistant/amd64-base-python:3.13-alpine3.22
|
||||||
i386: ghcr.io/home-assistant/i386-base-python:3.13-alpine3.22-2025.11.1
|
i386: ghcr.io/home-assistant/i386-base-python:3.13-alpine3.22
|
||||||
|
codenotary:
|
||||||
|
signer: notary@home-assistant.io
|
||||||
|
base_image: notary@home-assistant.io
|
||||||
cosign:
|
cosign:
|
||||||
base_identity: https://github.com/home-assistant/docker-base/.*
|
base_identity: https://github.com/home-assistant/docker-base/.*
|
||||||
identity: https://github.com/home-assistant/supervisor/.*
|
identity: https://github.com/home-assistant/supervisor/.*
|
||||||
|
|||||||
@@ -306,8 +306,6 @@ class DeviceType(IntEnum):
|
|||||||
VLAN = 11
|
VLAN = 11
|
||||||
TUN = 16
|
TUN = 16
|
||||||
VETH = 20
|
VETH = 20
|
||||||
WIREGUARD = 29
|
|
||||||
LOOPBACK = 32
|
|
||||||
|
|
||||||
|
|
||||||
class WirelessMethodType(IntEnum):
|
class WirelessMethodType(IntEnum):
|
||||||
|
|||||||
@@ -134,10 +134,9 @@ class NetworkManager(DBusInterfaceProxy):
|
|||||||
async def check_connectivity(self, *, force: bool = False) -> ConnectivityState:
|
async def check_connectivity(self, *, force: bool = False) -> ConnectivityState:
|
||||||
"""Check the connectivity of the host."""
|
"""Check the connectivity of the host."""
|
||||||
if force:
|
if force:
|
||||||
return ConnectivityState(
|
return await self.connected_dbus.call("check_connectivity")
|
||||||
await self.connected_dbus.call("check_connectivity")
|
else:
|
||||||
)
|
return await self.connected_dbus.get("connectivity")
|
||||||
return ConnectivityState(await self.connected_dbus.get("connectivity"))
|
|
||||||
|
|
||||||
async def connect(self, bus: MessageBus) -> None:
|
async def connect(self, bus: MessageBus) -> None:
|
||||||
"""Connect to system's D-Bus."""
|
"""Connect to system's D-Bus."""
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class NetworkConnection(DBusInterfaceProxy):
|
|||||||
@dbus_property
|
@dbus_property
|
||||||
def state(self) -> ConnectionStateType:
|
def state(self) -> ConnectionStateType:
|
||||||
"""Return the state of the connection."""
|
"""Return the state of the connection."""
|
||||||
return ConnectionStateType(self.properties[DBUS_ATTR_STATE])
|
return self.properties[DBUS_ATTR_STATE]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_flags(self) -> set[ConnectionStateFlags]:
|
def state_flags(self) -> set[ConnectionStateFlags]:
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""NetworkInterface object for Network Manager."""
|
"""NetworkInterface object for Network Manager."""
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
@@ -24,8 +23,6 @@ from .connection import NetworkConnection
|
|||||||
from .setting import NetworkSetting
|
from .setting import NetworkSetting
|
||||||
from .wireless import NetworkWireless
|
from .wireless import NetworkWireless
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkInterface(DBusInterfaceProxy):
|
class NetworkInterface(DBusInterfaceProxy):
|
||||||
"""NetworkInterface object represents Network Manager Device objects.
|
"""NetworkInterface object represents Network Manager Device objects.
|
||||||
@@ -60,15 +57,7 @@ class NetworkInterface(DBusInterfaceProxy):
|
|||||||
@dbus_property
|
@dbus_property
|
||||||
def type(self) -> DeviceType:
|
def type(self) -> DeviceType:
|
||||||
"""Return interface type."""
|
"""Return interface type."""
|
||||||
try:
|
return self.properties[DBUS_ATTR_DEVICE_TYPE]
|
||||||
return DeviceType(self.properties[DBUS_ATTR_DEVICE_TYPE])
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Unknown device type %s for %s, treating as UNKNOWN",
|
|
||||||
self.properties[DBUS_ATTR_DEVICE_TYPE],
|
|
||||||
self.object_path,
|
|
||||||
)
|
|
||||||
return DeviceType.UNKNOWN
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@dbus_property
|
@dbus_property
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class JobCondition(StrEnum):
|
|||||||
PLUGINS_UPDATED = "plugins_updated"
|
PLUGINS_UPDATED = "plugins_updated"
|
||||||
RUNNING = "running"
|
RUNNING = "running"
|
||||||
SUPERVISOR_UPDATED = "supervisor_updated"
|
SUPERVISOR_UPDATED = "supervisor_updated"
|
||||||
ARCHITECTURE_SUPPORTED = "architecture_supported"
|
|
||||||
|
|
||||||
|
|
||||||
class JobConcurrency(StrEnum):
|
class JobConcurrency(StrEnum):
|
||||||
|
|||||||
@@ -441,14 +441,6 @@ class Job(CoreSysAttributes):
|
|||||||
raise JobConditionException(
|
raise JobConditionException(
|
||||||
f"'{method_name}' blocked from execution, supervisor needs to be updated first"
|
f"'{method_name}' blocked from execution, supervisor needs to be updated first"
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
JobCondition.ARCHITECTURE_SUPPORTED in used_conditions
|
|
||||||
and UnsupportedReason.SYSTEM_ARCHITECTURE
|
|
||||||
in coresys.sys_resolution.unsupported
|
|
||||||
):
|
|
||||||
raise JobConditionException(
|
|
||||||
f"'{method_name}' blocked from execution, unsupported system architecture"
|
|
||||||
)
|
|
||||||
|
|
||||||
if JobCondition.PLUGINS_UPDATED in used_conditions and (
|
if JobCondition.PLUGINS_UPDATED in used_conditions and (
|
||||||
out_of_date := [
|
out_of_date := [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""A collection of tasks."""
|
"""A collection of tasks."""
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -13,6 +14,7 @@ from ..exceptions import (
|
|||||||
BackupFileNotFoundError,
|
BackupFileNotFoundError,
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
ObserverError,
|
ObserverError,
|
||||||
|
SupervisorUpdateError,
|
||||||
)
|
)
|
||||||
from ..homeassistant.const import LANDINGPAGE, WSType
|
from ..homeassistant.const import LANDINGPAGE, WSType
|
||||||
from ..jobs.const import JobConcurrency
|
from ..jobs.const import JobConcurrency
|
||||||
@@ -161,7 +163,6 @@ class Tasks(CoreSysAttributes):
|
|||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.OS_SUPPORTED,
|
JobCondition.OS_SUPPORTED,
|
||||||
JobCondition.RUNNING,
|
JobCondition.RUNNING,
|
||||||
JobCondition.ARCHITECTURE_SUPPORTED,
|
|
||||||
],
|
],
|
||||||
concurrency=JobConcurrency.REJECT,
|
concurrency=JobConcurrency.REJECT,
|
||||||
)
|
)
|
||||||
@@ -174,7 +175,11 @@ class Tasks(CoreSysAttributes):
|
|||||||
"Found new Supervisor version %s, updating",
|
"Found new Supervisor version %s, updating",
|
||||||
self.sys_supervisor.latest_version,
|
self.sys_supervisor.latest_version,
|
||||||
)
|
)
|
||||||
await self.sys_supervisor.update()
|
|
||||||
|
# Errors are logged by the exceptions, we can't really do something
|
||||||
|
# if an update fails here.
|
||||||
|
with suppress(SupervisorUpdateError):
|
||||||
|
await self.sys_supervisor.update()
|
||||||
|
|
||||||
async def _watchdog_homeassistant_api(self):
|
async def _watchdog_homeassistant_api(self):
|
||||||
"""Create scheduler task for monitoring running state of API.
|
"""Create scheduler task for monitoring running state of API.
|
||||||
|
|||||||
@@ -23,5 +23,4 @@ PLUGIN_UPDATE_CONDITIONS = [
|
|||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.SUPERVISOR_UPDATED,
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
JobCondition.ARCHITECTURE_SUPPORTED,
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class UnsupportedReason(StrEnum):
|
|||||||
SYSTEMD_JOURNAL = "systemd_journal"
|
SYSTEMD_JOURNAL = "systemd_journal"
|
||||||
SYSTEMD_RESOLVED = "systemd_resolved"
|
SYSTEMD_RESOLVED = "systemd_resolved"
|
||||||
VIRTUALIZATION_IMAGE = "virtualization_image"
|
VIRTUALIZATION_IMAGE = "virtualization_image"
|
||||||
SYSTEM_ARCHITECTURE = "system_architecture"
|
|
||||||
|
|
||||||
|
|
||||||
class UnhealthyReason(StrEnum):
|
class UnhealthyReason(StrEnum):
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Evaluation class for system architecture support."""
|
|
||||||
|
|
||||||
from ...const import CoreState
|
|
||||||
from ...coresys import CoreSys
|
|
||||||
from ..const import UnsupportedReason
|
|
||||||
from .base import EvaluateBase
|
|
||||||
|
|
||||||
|
|
||||||
def setup(coresys: CoreSys) -> EvaluateBase:
|
|
||||||
"""Initialize evaluation-setup function."""
|
|
||||||
return EvaluateSystemArchitecture(coresys)
|
|
||||||
|
|
||||||
|
|
||||||
class EvaluateSystemArchitecture(EvaluateBase):
|
|
||||||
"""Evaluate if the current Supervisor architecture is supported."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def reason(self) -> UnsupportedReason:
|
|
||||||
"""Return a UnsupportedReason enum."""
|
|
||||||
return UnsupportedReason.SYSTEM_ARCHITECTURE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def on_failure(self) -> str:
|
|
||||||
"""Return a string that is printed when self.evaluate is True."""
|
|
||||||
return "System architecture is no longer supported. Move to a supported system architecture."
|
|
||||||
|
|
||||||
@property
|
|
||||||
def states(self) -> list[CoreState]:
|
|
||||||
"""Return a list of valid states when this evaluation can run."""
|
|
||||||
return [CoreState.INITIALIZE]
|
|
||||||
|
|
||||||
async def evaluate(self):
|
|
||||||
"""Run evaluation."""
|
|
||||||
return self.sys_host.info.sys_arch.supervisor in {
|
|
||||||
"i386",
|
|
||||||
"armhf",
|
|
||||||
"armv7",
|
|
||||||
}
|
|
||||||
@@ -242,10 +242,9 @@ class Updater(FileConfiguration, CoreSysAttributes):
|
|||||||
@Job(
|
@Job(
|
||||||
name="updater_fetch_data",
|
name="updater_fetch_data",
|
||||||
conditions=[
|
conditions=[
|
||||||
JobCondition.ARCHITECTURE_SUPPORTED,
|
|
||||||
JobCondition.INTERNET_SYSTEM,
|
JobCondition.INTERNET_SYSTEM,
|
||||||
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
|
|
||||||
JobCondition.OS_SUPPORTED,
|
JobCondition.OS_SUPPORTED,
|
||||||
|
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
|
||||||
],
|
],
|
||||||
on_condition=UpdaterJobError,
|
on_condition=UpdaterJobError,
|
||||||
throttle_period=timedelta(seconds=30),
|
throttle_period=timedelta(seconds=30),
|
||||||
|
|||||||
@@ -184,20 +184,3 @@ async def test_interface_becomes_unmanaged(
|
|||||||
assert wireless.is_connected is False
|
assert wireless.is_connected is False
|
||||||
assert eth0.connection is None
|
assert eth0.connection is None
|
||||||
assert connection.is_connected is False
|
assert connection.is_connected is False
|
||||||
|
|
||||||
|
|
||||||
async def test_unknown_device_type(
|
|
||||||
device_eth0_service: DeviceService, dbus_session_bus: MessageBus
|
|
||||||
):
|
|
||||||
"""Test unknown device types are handled gracefully."""
|
|
||||||
interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/1")
|
|
||||||
await interface.connect(dbus_session_bus)
|
|
||||||
|
|
||||||
# Emit an unknown device type (e.g., 1000 which doesn't exist in the enum)
|
|
||||||
device_eth0_service.emit_properties_changed({"DeviceType": 1000})
|
|
||||||
await device_eth0_service.ping()
|
|
||||||
|
|
||||||
# Should return UNKNOWN instead of crashing
|
|
||||||
assert interface.type == DeviceType.UNKNOWN
|
|
||||||
# Wireless should be None since it's not a wireless device
|
|
||||||
assert interface.wireless is None
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
"""Test evaluation supported system architectures."""
|
|
||||||
|
|
||||||
from unittest.mock import PropertyMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from supervisor.const import CoreState
|
|
||||||
from supervisor.coresys import CoreSys
|
|
||||||
from supervisor.resolution.evaluations.system_architecture import (
|
|
||||||
EvaluateSystemArchitecture,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arch", ["i386", "armhf", "armv7"])
|
|
||||||
async def test_evaluation_unsupported_architectures(
|
|
||||||
coresys: CoreSys,
|
|
||||||
arch: str,
|
|
||||||
):
|
|
||||||
"""Test evaluation of unsupported system architectures."""
|
|
||||||
system_architecture = EvaluateSystemArchitecture(coresys)
|
|
||||||
await coresys.core.set_state(CoreState.INITIALIZE)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
type(coresys.supervisor), "arch", PropertyMock(return_value=arch)
|
|
||||||
):
|
|
||||||
await system_architecture()
|
|
||||||
assert system_architecture.reason in coresys.resolution.unsupported
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("arch", ["amd64", "aarch64"])
|
|
||||||
async def test_evaluation_supported_architectures(
|
|
||||||
coresys: CoreSys,
|
|
||||||
arch: str,
|
|
||||||
):
|
|
||||||
"""Test evaluation of supported system architectures."""
|
|
||||||
system_architecture = EvaluateSystemArchitecture(coresys)
|
|
||||||
await coresys.core.set_state(CoreState.INITIALIZE)
|
|
||||||
|
|
||||||
with patch.object(
|
|
||||||
type(coresys.supervisor), "arch", PropertyMock(return_value=arch)
|
|
||||||
):
|
|
||||||
await system_architecture()
|
|
||||||
assert system_architecture.reason not in coresys.resolution.unsupported
|
|
||||||
Reference in New Issue
Block a user