Compare commits

..

1 Commits

Author SHA1 Message Date
Stefan Agner
dbb4eab381 Handle update errors in automatic Supervisor update task
Wrap the Supervisor auto-update call with suppress(SupervisorUpdateError)
to prevent unhandled exceptions from propagating. When an automatic update
fails, errors are already logged by the exception handlers, and there's no
meaningful recovery action the scheduler task can take.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:32:44 +01:00
35 changed files with 185 additions and 332 deletions

View File

@@ -34,9 +34,6 @@ on:
env: env:
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.13"
COSIGN_VERSION: "v2.5.3"
CRANE_VERSION: "v0.20.7"
CRANE_SHA256: "8ef3564d264e6b5ca93f7b7f5652704c4dd29d33935aff6947dd5adefd05953e"
BUILD_NAME: supervisor BUILD_NAME: supervisor
BUILD_TYPE: supervisor BUILD_TYPE: supervisor
@@ -129,7 +126,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@@ -137,7 +134,7 @@ jobs:
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with: with:
cosign-release: ${{ env.COSIGN_VERSION }} cosign-release: "v2.5.3"
- name: Install dirhash and calc hash - name: Install dirhash and calc hash
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
@@ -176,7 +173,7 @@ jobs:
version: version:
name: Update version name: Update version
needs: ["init", "run_supervisor", "retag_deprecated"] needs: ["init", "run_supervisor"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
@@ -355,50 +352,3 @@ jobs:
- name: Get supervisor logs on failiure - name: Get supervisor logs on failiure
if: ${{ cancelled() || failure() }} if: ${{ cancelled() || failure() }}
run: docker logs hassio_supervisor run: docker logs hassio_supervisor
retag_deprecated:
needs: ["build", "init"]
name: Re-tag deprecated ${{ matrix.arch }} images
if: needs.init.outputs.publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
strategy:
matrix:
arch: ["armhf", "armv7", "i386"]
env:
# Last available release for deprecated architectures
FROZEN_VERSION: "2025.11.5"
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: ${{ env.COSIGN_VERSION }}
- name: Install crane
run: |
curl -sLO https://github.com/google/go-containerregistry/releases/download/${{ env.CRANE_VERSION }}/go-containerregistry_Linux_x86_64.tar.gz
echo "${{ env.CRANE_SHA256 }} go-containerregistry_Linux_x86_64.tar.gz" | sha256sum -c -
tar xzf go-containerregistry_Linux_x86_64.tar.gz crane
sudo mv crane /usr/local/bin/
- name: Re-tag deprecated image with updated version label
run: |
crane auth login ghcr.io -u ${{ github.repository_owner }} -p ${{ secrets.GITHUB_TOKEN }}
crane mutate \
--label io.hass.version=${{ needs.init.outputs.version }} \
--tag ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ needs.init.outputs.version }} \
ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ env.FROZEN_VERSION }}
- name: Sign image with Cosign
run: |
cosign sign --yes ghcr.io/home-assistant/${{ matrix.arch }}-hassio-supervisor:${{ needs.init.outputs.version }}

View File

@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python - name: Set up Python
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
@@ -70,7 +70,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -113,7 +113,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -171,7 +171,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -215,7 +215,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -259,7 +259,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -295,7 +295,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -341,7 +341,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}
@@ -400,7 +400,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ needs.prepare.outputs.python-version }}

View File

@@ -68,7 +68,7 @@ jobs:
run: | run: |
rm -f supervisor/api/panel/home_assistant_frontend_supervisor-*.tar.gz rm -f supervisor/api/panel/home_assistant_frontend_supervisor-*.tar.gz
- name: Create PR - name: Create PR
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
commit-message: "Update frontend to version ${{ needs.check-version.outputs.latest_version }}" commit-message: "Update frontend to version ${{ needs.check-version.outputs.latest_version }}"
branch: autoupdate-frontend branch: autoupdate-frontend

View File

@@ -1,7 +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
amd64: ghcr.io/home-assistant/amd64-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
amd64: ghcr.io/home-assistant/amd64-base-python:3.13-alpine3.22
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/.*

View File

@@ -4,7 +4,7 @@ aiohttp==3.13.2
atomicwrites-homeassistant==1.4.1 atomicwrites-homeassistant==1.4.1
attrs==25.4.0 attrs==25.4.0
awesomeversion==25.8.0 awesomeversion==25.8.0
backports.zstd==1.1.0 backports.zstd==1.0.0
blockbuster==1.5.25 blockbuster==1.5.25
brotli==1.2.0 brotli==1.2.0
ciso8601==2.3.3 ciso8601==2.3.3
@@ -25,8 +25,8 @@ pyudev==0.24.4
PyYAML==6.0.3 PyYAML==6.0.3
requests==2.32.5 requests==2.32.5
securetar==2025.2.1 securetar==2025.2.1
sentry-sdk==2.46.0 sentry-sdk==2.45.0
setuptools==80.9.0 setuptools==80.9.0
voluptuous==0.15.2 voluptuous==0.15.2
dbus-fast==3.1.2 dbus-fast==2.45.1
zlib-fast==0.2.1 zlib-fast==0.2.1

View File

@@ -1,16 +1,16 @@
astroid==4.0.2 astroid==4.0.2
coverage==7.12.0 coverage==7.12.0
mypy==1.18.2 mypy==1.18.2
pre-commit==4.5.0 pre-commit==4.4.0
pylint==4.0.3 pylint==4.0.3
pytest-aiohttp==1.1.0 pytest-aiohttp==1.1.0
pytest-asyncio==1.3.0 pytest-asyncio==1.3.0
pytest-cov==7.0.0 pytest-cov==7.0.0
pytest-timeout==2.4.0 pytest-timeout==2.4.0
pytest==9.0.1 pytest==9.0.1
ruff==0.14.6 ruff==0.14.5
time-machine==3.1.0 time-machine==3.0.0
types-docker==7.1.0.20251125 types-docker==7.1.0.20251009
types-pyyaml==6.0.12.20250915 types-pyyaml==6.0.12.20250915
types-requests==2.32.4.20250913 types-requests==2.32.4.20250913
urllib3==2.5.0 urllib3==2.5.0

View File

@@ -343,14 +343,10 @@ class APIHost(CoreSysAttributes):
disk = self.sys_hardware.disk disk = self.sys_hardware.disk
total, _, free = await self.sys_run_in_executor( total, used, _ = await self.sys_run_in_executor(
disk.disk_usage, self.sys_config.path_supervisor disk.disk_usage, self.sys_config.path_supervisor
) )
# Calculate used by subtracting free makes sure we include reserved space
# in used space reporting.
used = total - free
known_paths = await self.sys_run_in_executor( known_paths = await self.sys_run_in_executor(
disk.get_dir_sizes, disk.get_dir_sizes,
{ {

View File

@@ -63,10 +63,12 @@ def json_loads(data: Any) -> dict[str, Any]:
def api_process(method): def api_process(method):
"""Wrap function with true/false calls to rest api.""" """Wrap function with true/false calls to rest api."""
async def wrap_api(*args, **kwargs) -> web.Response | web.StreamResponse: async def wrap_api(
api: CoreSysAttributes, *args, **kwargs
) -> web.Response | web.StreamResponse:
"""Return API information.""" """Return API information."""
try: try:
answer = await method(*args, **kwargs) answer = await method(api, *args, **kwargs)
except BackupFileNotFoundError as err: except BackupFileNotFoundError as err:
return api_return_error(err, status=404) return api_return_error(err, status=404)
except APIError as err: except APIError as err:
@@ -107,10 +109,12 @@ def api_process_raw(content, *, error_type=None):
def wrap_method(method): def wrap_method(method):
"""Wrap function with raw output to rest api.""" """Wrap function with raw output to rest api."""
async def wrap_api(*args, **kwargs) -> web.Response | web.StreamResponse: async def wrap_api(
api: CoreSysAttributes, *args, **kwargs
) -> web.Response | web.StreamResponse:
"""Return api information.""" """Return api information."""
try: try:
msg_data = await method(*args, **kwargs) msg_data = await method(api, *args, **kwargs)
except APIError as err: except APIError as err:
return api_return_error( return api_return_error(
err, err,

View File

@@ -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):

View File

@@ -115,7 +115,7 @@ class DBusManager(CoreSysAttributes):
async def load(self) -> None: async def load(self) -> None:
"""Connect interfaces to D-Bus.""" """Connect interfaces to D-Bus."""
if not await self.sys_run_in_executor(SOCKET_DBUS.exists): if not SOCKET_DBUS.exists():
_LOGGER.error( _LOGGER.error(
"No D-Bus support on Host. Disabled any kind of host control!" "No D-Bus support on Host. Disabled any kind of host control!"
) )

View File

@@ -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."""

View File

@@ -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]:

View File

@@ -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

View File

@@ -75,7 +75,7 @@ class Resolved(DBusInterfaceProxy):
@dbus_property @dbus_property
def current_dns_server( def current_dns_server(
self, self,
) -> tuple[int, DNSAddressFamily, bytes] | None: ) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
"""Return current DNS server.""" """Return current DNS server."""
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER] return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER]
@@ -83,7 +83,7 @@ class Resolved(DBusInterfaceProxy):
@dbus_property @dbus_property
def current_dns_server_ex( def current_dns_server_ex(
self, self,
) -> tuple[int, DNSAddressFamily, bytes, int, str] | None: ) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
"""Return current DNS server including port and server name.""" """Return current DNS server including port and server name."""
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER_EX] return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER_EX]

View File

@@ -70,7 +70,7 @@ class SystemdUnit(DBusInterface):
@dbus_connected @dbus_connected
async def get_active_state(self) -> UnitActiveState: async def get_active_state(self) -> UnitActiveState:
"""Get active state of the unit.""" """Get active state of the unit."""
return UnitActiveState(await self.connected_dbus.Unit.get("active_state")) return await self.connected_dbus.Unit.get("active_state")
@dbus_connected @dbus_connected
def properties_changed(self) -> DBusSignalWrapper: def properties_changed(self) -> DBusSignalWrapper:

View File

@@ -9,7 +9,7 @@ from dbus_fast import Variant
from .const import EncryptType, EraseMode from .const import EncryptType, EraseMode
def udisks2_bytes_to_path(path_bytes: bytes) -> Path: def udisks2_bytes_to_path(path_bytes: bytearray) -> Path:
"""Convert bytes to path object without null character on end.""" """Convert bytes to path object without null character on end."""
if path_bytes and path_bytes[-1] == 0: if path_bytes and path_bytes[-1] == 0:
return Path(path_bytes[:-1].decode()) return Path(path_bytes[:-1].decode())
@@ -73,7 +73,7 @@ FormatOptionsDataType = TypedDict(
{ {
"label": NotRequired[str], "label": NotRequired[str],
"take-ownership": NotRequired[bool], "take-ownership": NotRequired[bool],
"encrypt.passphrase": NotRequired[bytes], "encrypt.passphrase": NotRequired[bytearray],
"encrypt.type": NotRequired[str], "encrypt.type": NotRequired[str],
"erase": NotRequired[str], "erase": NotRequired[str],
"update-partition-type": NotRequired[bool], "update-partition-type": NotRequired[bool],

View File

@@ -76,25 +76,15 @@ class DockerInfo:
storage: str = attr.ib() storage: str = attr.ib()
logging: str = attr.ib() logging: str = attr.ib()
cgroup: str = attr.ib() cgroup: str = attr.ib()
support_cpu_realtime: bool = attr.ib()
@staticmethod @staticmethod
async def new(data: dict[str, Any]) -> DockerInfo: def new(data: dict[str, Any]):
"""Create a object from docker info.""" """Create a object from docker info."""
# Check if CONFIG_RT_GROUP_SCHED is loaded (blocking I/O in executor)
cpu_rt_file_exists = await asyncio.get_running_loop().run_in_executor(
None, Path("/sys/fs/cgroup/cpu/cpu.rt_runtime_us").exists
)
cpu_rt_supported = (
cpu_rt_file_exists and os.environ.get(ENV_SUPERVISOR_CPU_RT) == "1"
)
return DockerInfo( return DockerInfo(
AwesomeVersion(data.get("ServerVersion", "0.0.0")), AwesomeVersion(data.get("ServerVersion", "0.0.0")),
data.get("Driver", "unknown"), data.get("Driver", "unknown"),
data.get("LoggingDriver", "unknown"), data.get("LoggingDriver", "unknown"),
data.get("CgroupVersion", "1"), data.get("CgroupVersion", "1"),
cpu_rt_supported,
) )
@property @property
@@ -105,6 +95,13 @@ class DockerInfo:
except AwesomeVersionCompareException: except AwesomeVersionCompareException:
return False return False
@property
def support_cpu_realtime(self) -> bool:
"""Return true, if CONFIG_RT_GROUP_SCHED is loaded."""
if not Path("/sys/fs/cgroup/cpu/cpu.rt_runtime_us").exists():
return False
return bool(os.environ.get(ENV_SUPERVISOR_CPU_RT) == "1")
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PullProgressDetail: class PullProgressDetail:
@@ -237,7 +234,7 @@ class DockerAPI(CoreSysAttributes):
timeout=900, timeout=900,
), ),
) )
self._info = await DockerInfo.new(self.dockerpy.info()) self._info = DockerInfo.new(self.dockerpy.info())
await self.config.read_data() await self.config.read_data()
self._network = await DockerNetwork(self.dockerpy).post_init( self._network = await DockerNetwork(self.dockerpy).post_init(
self.config.enable_ipv6, self.config.mtu self.config.enable_ipv6, self.config.mtu

View File

@@ -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):

View File

@@ -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 := [

View File

@@ -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,6 +175,10 @@ 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,
) )
# 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() await self.sys_supervisor.update()
async def _watchdog_homeassistant_api(self): async def _watchdog_homeassistant_api(self):

View File

@@ -135,7 +135,7 @@ class Mount(CoreSysAttributes, ABC):
@property @property
def state(self) -> UnitActiveState | None: def state(self) -> UnitActiveState | None:
"""Get state of mount.""" """Get state of mount."""
return UnitActiveState(self._state) if self._state is not None else None return self._state
@cached_property @cached_property
def local_where(self) -> Path: def local_where(self) -> Path:

View File

@@ -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,
] ]

View File

@@ -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):

View File

@@ -5,6 +5,8 @@ from ...coresys import CoreSys
from ..const import UnsupportedReason from ..const import UnsupportedReason
from .base import EvaluateBase from .base import EvaluateBase
SUPPORTED_OS = ["Debian GNU/Linux 12 (bookworm)"]
def setup(coresys: CoreSys) -> EvaluateBase: def setup(coresys: CoreSys) -> EvaluateBase:
"""Initialize evaluation-setup function.""" """Initialize evaluation-setup function."""
@@ -31,4 +33,6 @@ class EvaluateOperatingSystem(EvaluateBase):
async def evaluate(self) -> bool: async def evaluate(self) -> bool:
"""Run evaluation.""" """Run evaluation."""
return not self.sys_os.available if self.sys_os.available:
return False
return self.sys_host.info.operating_system not in SUPPORTED_OS

View File

@@ -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",
}

View File

@@ -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),

View File

@@ -7,7 +7,13 @@ from collections.abc import Awaitable, Callable
import logging import logging
from typing import Any, Protocol, cast from typing import Any, Protocol, cast
from dbus_fast import ErrorType, InvalidIntrospectionError, Message, MessageType from dbus_fast import (
ErrorType,
InvalidIntrospectionError,
Message,
MessageType,
Variant,
)
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.aio.proxy_object import ProxyInterface, ProxyObject from dbus_fast.aio.proxy_object import ProxyInterface, ProxyObject
from dbus_fast.errors import DBusError as DBusFastDBusError from dbus_fast.errors import DBusError as DBusFastDBusError
@@ -259,7 +265,7 @@ class DBus:
""" """
async def sync_property_change( async def sync_property_change(
prop_interface: str, changed: dict[str, Any], invalidated: list[str] prop_interface: str, changed: dict[str, Variant], invalidated: list[str]
) -> None: ) -> None:
"""Sync property changes to cache.""" """Sync property changes to cache."""
if interface != prop_interface: if interface != prop_interface:

View File

@@ -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

View File

@@ -41,51 +41,51 @@ async def test_dbus_resolved_info(
assert resolved.dns_over_tls == DNSOverTLSEnabled.NO assert resolved.dns_over_tls == DNSOverTLSEnabled.NO
assert len(resolved.dns) == 2 assert len(resolved.dns) == 2
assert resolved.dns[0] == (0, 2, inet_aton("127.0.0.1")) assert resolved.dns[0] == [0, 2, inet_aton("127.0.0.1")]
assert resolved.dns[1] == (0, 10, inet_pton(AF_INET6, "::1")) assert resolved.dns[1] == [0, 10, inet_pton(AF_INET6, "::1")]
assert len(resolved.dns_ex) == 2 assert len(resolved.dns_ex) == 2
assert resolved.dns_ex[0] == (0, 2, inet_aton("127.0.0.1"), 0, "") assert resolved.dns_ex[0] == [0, 2, inet_aton("127.0.0.1"), 0, ""]
assert resolved.dns_ex[1] == (0, 10, inet_pton(AF_INET6, "::1"), 0, "") assert resolved.dns_ex[1] == [0, 10, inet_pton(AF_INET6, "::1"), 0, ""]
assert len(resolved.fallback_dns) == 2 assert len(resolved.fallback_dns) == 2
assert resolved.fallback_dns[0] == (0, 2, inet_aton("1.1.1.1")) assert resolved.fallback_dns[0] == [0, 2, inet_aton("1.1.1.1")]
assert resolved.fallback_dns[1] == ( assert resolved.fallback_dns[1] == [
0, 0,
10, 10,
inet_pton(AF_INET6, "2606:4700:4700::1111"), inet_pton(AF_INET6, "2606:4700:4700::1111"),
) ]
assert len(resolved.fallback_dns_ex) == 2 assert len(resolved.fallback_dns_ex) == 2
assert resolved.fallback_dns_ex[0] == ( assert resolved.fallback_dns_ex[0] == [
0, 0,
2, 2,
inet_aton("1.1.1.1"), inet_aton("1.1.1.1"),
0, 0,
"cloudflare-dns.com", "cloudflare-dns.com",
) ]
assert resolved.fallback_dns_ex[1] == ( assert resolved.fallback_dns_ex[1] == [
0, 0,
10, 10,
inet_pton(AF_INET6, "2606:4700:4700::1111"), inet_pton(AF_INET6, "2606:4700:4700::1111"),
0, 0,
"cloudflare-dns.com", "cloudflare-dns.com",
) ]
assert resolved.current_dns_server == (0, 2, inet_aton("127.0.0.1")) assert resolved.current_dns_server == [0, 2, inet_aton("127.0.0.1")]
assert resolved.current_dns_server_ex == ( assert resolved.current_dns_server_ex == [
0, 0,
2, 2,
inet_aton("127.0.0.1"), inet_aton("127.0.0.1"),
0, 0,
"", "",
) ]
assert len(resolved.domains) == 1 assert len(resolved.domains) == 1
assert resolved.domains[0] == (0, "local.hass.io", False) assert resolved.domains[0] == [0, "local.hass.io", False]
assert resolved.transaction_statistics == (0, 100000) assert resolved.transaction_statistics == [0, 100000]
assert resolved.cache_statistics == (10, 50000, 10000) assert resolved.cache_statistics == [10, 50000, 10000]
assert resolved.dnssec == DNSSECValidation.NO assert resolved.dnssec == DNSSECValidation.NO
assert resolved.dnssec_statistics == (0, 0, 0, 0) assert resolved.dnssec_statistics == [0, 0, 0, 0]
assert resolved.dnssec_supported is False assert resolved.dnssec_supported is False
assert resolved.dnssec_negative_trust_anchors == [ assert resolved.dnssec_negative_trust_anchors == [
"168.192.in-addr.arpa", "168.192.in-addr.arpa",

View File

@@ -185,10 +185,10 @@ async def test_start_transient_unit(
"tmp-test.mount", "tmp-test.mount",
"fail", "fail",
[ [
("Description", Variant("s", "Test")), ["Description", Variant("s", "Test")],
("What", Variant("s", "//homeassistant/config")), ["What", Variant("s", "//homeassistant/config")],
("Type", Variant("s", "cifs")), ["Type", Variant("s", "cifs")],
("Options", Variant("s", "username=homeassistant,password=password")), ["Options", Variant("s", "username=homeassistant,password=password")],
], ],
[], [],
) )

View File

@@ -45,8 +45,8 @@ class Resolved(DBusServiceMock):
def DNS(self) -> "a(iiay)": def DNS(self) -> "a(iiay)":
"""Get DNS.""" """Get DNS."""
return [ return [
(0, 2, bytes([127, 0, 0, 1])), [0, 2, bytes([127, 0, 0, 1])],
( [
0, 0,
10, 10,
bytes( bytes(
@@ -69,15 +69,15 @@ class Resolved(DBusServiceMock):
0x1, 0x1,
] ]
), ),
), ],
] ]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def DNSEx(self) -> "a(iiayqs)": def DNSEx(self) -> "a(iiayqs)":
"""Get DNSEx.""" """Get DNSEx."""
return [ return [
(0, 2, bytes([127, 0, 0, 1]), 0, ""), [0, 2, bytes([127, 0, 0, 1]), 0, ""],
( [
0, 0,
10, 10,
bytes( bytes(
@@ -102,15 +102,15 @@ class Resolved(DBusServiceMock):
), ),
0, 0,
"", "",
), ],
] ]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def FallbackDNS(self) -> "a(iiay)": def FallbackDNS(self) -> "a(iiay)":
"""Get FallbackDNS.""" """Get FallbackDNS."""
return [ return [
(0, 2, bytes([1, 1, 1, 1])), [0, 2, bytes([1, 1, 1, 1])],
( [
0, 0,
10, 10,
bytes( bytes(
@@ -133,15 +133,15 @@ class Resolved(DBusServiceMock):
0x11, 0x11,
] ]
), ),
), ],
] ]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def FallbackDNSEx(self) -> "a(iiayqs)": def FallbackDNSEx(self) -> "a(iiayqs)":
"""Get FallbackDNSEx.""" """Get FallbackDNSEx."""
return [ return [
(0, 2, bytes([1, 1, 1, 1]), 0, "cloudflare-dns.com"), [0, 2, bytes([1, 1, 1, 1]), 0, "cloudflare-dns.com"],
( [
0, 0,
10, 10,
bytes( bytes(
@@ -166,33 +166,33 @@ class Resolved(DBusServiceMock):
), ),
0, 0,
"cloudflare-dns.com", "cloudflare-dns.com",
), ],
] ]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def CurrentDNSServer(self) -> "(iiay)": def CurrentDNSServer(self) -> "(iiay)":
"""Get CurrentDNSServer.""" """Get CurrentDNSServer."""
return (0, 2, bytes([127, 0, 0, 1])) return [0, 2, bytes([127, 0, 0, 1])]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def CurrentDNSServerEx(self) -> "(iiayqs)": def CurrentDNSServerEx(self) -> "(iiayqs)":
"""Get CurrentDNSServerEx.""" """Get CurrentDNSServerEx."""
return (0, 2, bytes([127, 0, 0, 1]), 0, "") return [0, 2, bytes([127, 0, 0, 1]), 0, ""]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def Domains(self) -> "a(isb)": def Domains(self) -> "a(isb)":
"""Get Domains.""" """Get Domains."""
return [(0, "local.hass.io", False)] return [[0, "local.hass.io", False]]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def TransactionStatistics(self) -> "(tt)": def TransactionStatistics(self) -> "(tt)":
"""Get TransactionStatistics.""" """Get TransactionStatistics."""
return (0, 100000) return [0, 100000]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def CacheStatistics(self) -> "(ttt)": def CacheStatistics(self) -> "(ttt)":
"""Get CacheStatistics.""" """Get CacheStatistics."""
return (10, 50000, 10000) return [10, 50000, 10000]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def DNSSEC(self) -> "s": def DNSSEC(self) -> "s":
@@ -202,7 +202,7 @@ class Resolved(DBusServiceMock):
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def DNSSECStatistics(self) -> "(tttt)": def DNSSECStatistics(self) -> "(tttt)":
"""Get DNSSECStatistics.""" """Get DNSSECStatistics."""
return (0, 0, 0, 0) return [0, 0, 0, 0]
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def DNSSECSupported(self) -> "b": def DNSSECSupported(self) -> "b":

View File

@@ -119,10 +119,10 @@ async def test_load(
"mnt-data-supervisor-mounts-backup_test.mount", "mnt-data-supervisor-mounts-backup_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "noserverino,guest")), ["Options", Variant("s", "noserverino,guest")],
("Type", Variant("s", "cifs")), ["Type", Variant("s", "cifs")],
("Description", Variant("s", "Supervisor cifs mount: backup_test")), ["Description", Variant("s", "Supervisor cifs mount: backup_test")],
("What", Variant("s", "//backup.local/backups")), ["What", Variant("s", "//backup.local/backups")],
], ],
[], [],
), ),
@@ -130,10 +130,10 @@ async def test_load(
"mnt-data-supervisor-mounts-media_test.mount", "mnt-data-supervisor-mounts-media_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "soft,timeo=200")), ["Options", Variant("s", "soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: media_test")), ["Description", Variant("s", "Supervisor nfs mount: media_test")],
("What", Variant("s", "media.local:/media")), ["What", Variant("s", "media.local:/media")],
], ],
[], [],
), ),
@@ -141,12 +141,12 @@ async def test_load(
"mnt-data-supervisor-media-media_test.mount", "mnt-data-supervisor-media-media_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "bind")), ["Options", Variant("s", "bind")],
( [
"Description", "Description",
Variant("s", "Supervisor bind mount: bind_media_test"), Variant("s", "Supervisor bind mount: bind_media_test"),
), ],
("What", Variant("s", "/mnt/data/supervisor/mounts/media_test")), ["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
], ],
[], [],
), ),
@@ -198,10 +198,10 @@ async def test_load_share_mount(
"mnt-data-supervisor-mounts-share_test.mount", "mnt-data-supervisor-mounts-share_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "soft,timeo=200")), ["Options", Variant("s", "soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: share_test")), ["Description", Variant("s", "Supervisor nfs mount: share_test")],
("What", Variant("s", "share.local:/share")), ["What", Variant("s", "share.local:/share")],
], ],
[], [],
), ),
@@ -209,9 +209,9 @@ async def test_load_share_mount(
"mnt-data-supervisor-share-share_test.mount", "mnt-data-supervisor-share-share_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "bind")), ["Options", Variant("s", "bind")],
("Description", Variant("s", "Supervisor bind mount: bind_share_test")), ["Description", Variant("s", "Supervisor bind mount: bind_share_test")],
("What", Variant("s", "/mnt/data/supervisor/mounts/share_test")), ["What", Variant("s", "/mnt/data/supervisor/mounts/share_test")],
], ],
[], [],
), ),
@@ -318,12 +318,12 @@ async def test_mount_failed_during_load(
"mnt-data-supervisor-media-media_test.mount", "mnt-data-supervisor-media-media_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "ro,bind")), ["Options", Variant("s", "ro,bind")],
( [
"Description", "Description",
Variant("s", "Supervisor bind mount: emergency_media_test"), Variant("s", "Supervisor bind mount: emergency_media_test"),
), ],
("What", Variant("s", "/mnt/data/supervisor/emergency/media_test")), ["What", Variant("s", "/mnt/data/supervisor/emergency/media_test")],
], ],
[], [],
) )
@@ -634,10 +634,10 @@ async def test_reload_mounts_attempts_initial_mount(
"mnt-data-supervisor-mounts-media_test.mount", "mnt-data-supervisor-mounts-media_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "soft,timeo=200")), ["Options", Variant("s", "soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: media_test")), ["Description", Variant("s", "Supervisor nfs mount: media_test")],
("What", Variant("s", "media.local:/media")), ["What", Variant("s", "media.local:/media")],
], ],
[], [],
), ),
@@ -645,9 +645,9 @@ async def test_reload_mounts_attempts_initial_mount(
"mnt-data-supervisor-media-media_test.mount", "mnt-data-supervisor-media-media_test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "bind")), ["Options", Variant("s", "bind")],
("Description", Variant("s", "Supervisor bind mount: bind_media_test")), ["Description", Variant("s", "Supervisor bind mount: bind_media_test")],
("What", Variant("s", "/mnt/data/supervisor/mounts/media_test")), ["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
], ],
[], [],
), ),

View File

@@ -105,7 +105,7 @@ async def test_cifs_mount(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
( [
"Options", "Options",
Variant( Variant(
"s", "s",
@@ -117,10 +117,10 @@ async def test_cifs_mount(
] ]
), ),
), ),
), ],
("Type", Variant("s", "cifs")), ["Type", Variant("s", "cifs")],
("Description", Variant("s", "Supervisor cifs mount: test")), ["Description", Variant("s", "Supervisor cifs mount: test")],
("What", Variant("s", "//test.local/camera")), ["What", Variant("s", "//test.local/camera")],
], ],
[], [],
) )
@@ -177,10 +177,10 @@ async def test_cifs_mount_read_only(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "ro,noserverino,guest")), ["Options", Variant("s", "ro,noserverino,guest")],
("Type", Variant("s", "cifs")), ["Type", Variant("s", "cifs")],
("Description", Variant("s", "Supervisor cifs mount: test")), ["Description", Variant("s", "Supervisor cifs mount: test")],
("What", Variant("s", "//test.local/camera")), ["What", Variant("s", "//test.local/camera")],
], ],
[], [],
) )
@@ -237,10 +237,10 @@ async def test_nfs_mount(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "port=1234,soft,timeo=200")), ["Options", Variant("s", "port=1234,soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: test")), ["Description", Variant("s", "Supervisor nfs mount: test")],
("What", Variant("s", "test.local:/media/camera")), ["What", Variant("s", "test.local:/media/camera")],
], ],
[], [],
) )
@@ -283,10 +283,10 @@ async def test_nfs_mount_read_only(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "ro,port=1234,soft,timeo=200")), ["Options", Variant("s", "ro,port=1234,soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: test")), ["Description", Variant("s", "Supervisor nfs mount: test")],
("What", Variant("s", "test.local:/media/camera")), ["What", Variant("s", "test.local:/media/camera")],
], ],
[], [],
) )
@@ -331,10 +331,10 @@ async def test_load(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "noserverino,guest")), ["Options", Variant("s", "noserverino,guest")],
("Type", Variant("s", "cifs")), ["Type", Variant("s", "cifs")],
("Description", Variant("s", "Supervisor cifs mount: test")), ["Description", Variant("s", "Supervisor cifs mount: test")],
("What", Variant("s", "//test.local/share")), ["What", Variant("s", "//test.local/share")],
], ],
[], [],
) )
@@ -736,10 +736,10 @@ async def test_mount_fails_if_down(
"mnt-data-supervisor-mounts-test.mount", "mnt-data-supervisor-mounts-test.mount",
"fail", "fail",
[ [
("Options", Variant("s", "port=1234,soft,timeo=200")), ["Options", Variant("s", "port=1234,soft,timeo=200")],
("Type", Variant("s", "nfs")), ["Type", Variant("s", "nfs")],
("Description", Variant("s", "Supervisor nfs mount: test")), ["Description", Variant("s", "Supervisor nfs mount: test")],
("What", Variant("s", "test.local:/media/camera")), ["What", Variant("s", "test.local:/media/camera")],
], ],
[], [],
) )

View File

@@ -5,7 +5,10 @@ from unittest.mock import MagicMock, patch
from supervisor.const import CoreState from supervisor.const import CoreState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.resolution.evaluations.operating_system import EvaluateOperatingSystem from supervisor.resolution.evaluations.operating_system import (
SUPPORTED_OS,
EvaluateOperatingSystem,
)
async def test_evaluation(coresys: CoreSys): async def test_evaluation(coresys: CoreSys):
@@ -22,7 +25,13 @@ async def test_evaluation(coresys: CoreSys):
assert operating_system.reason in coresys.resolution.unsupported assert operating_system.reason in coresys.resolution.unsupported
coresys.os._available = True coresys.os._available = True
assert coresys.os.available await operating_system()
assert operating_system.reason not in coresys.resolution.unsupported
coresys.os._available = False
coresys.host._info = MagicMock(
operating_system=SUPPORTED_OS[0], timezone=None, timezone_tzinfo=None
)
await operating_system() await operating_system()
assert operating_system.reason not in coresys.resolution.unsupported assert operating_system.reason not in coresys.resolution.unsupported

View File

@@ -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