mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 07:06:30 +00:00
Add mount to supported features (#4301)
* Add mount to supported features * Typo in enable * Fix places mocking os available without version * Increase resilence of problematic repeat task test
This commit is contained in:
parent
fbb2776277
commit
f6c3bdb6a8
@ -589,3 +589,7 @@ class MountInvalidError(MountError):
|
||||
|
||||
class MountNotFound(MountError):
|
||||
"""Raise on mount not found."""
|
||||
|
||||
|
||||
class MountJobError(MountError, JobException):
|
||||
"""Raise on Mount job error."""
|
||||
|
@ -46,6 +46,7 @@ class HostFeature(str, Enum):
|
||||
HAOS = "haos"
|
||||
HOSTNAME = "hostname"
|
||||
JOURNAL = "journal"
|
||||
MOUNT = "mount"
|
||||
NETWORK = "network"
|
||||
OS_AGENT = "os_agent"
|
||||
REBOOT = "reboot"
|
||||
|
@ -3,7 +3,7 @@ from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
|
||||
from supervisor.host.logs import LogsControl
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
from ..const import BusEvent
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
@ -14,6 +14,7 @@ from .apparmor import AppArmorControl
|
||||
from .const import HostFeature
|
||||
from .control import SystemControl
|
||||
from .info import InfoCenter
|
||||
from .logs import LogsControl
|
||||
from .network import NetworkManager
|
||||
from .services import ServiceManager
|
||||
from .sound import SoundControl
|
||||
@ -110,6 +111,12 @@ class HostManager(CoreSysAttributes):
|
||||
if self.sys_dbus.udisks2.is_connected:
|
||||
features.append(HostFeature.DISK)
|
||||
|
||||
# Support added in OS10. For supervised, assume they can if systemd is connected
|
||||
if self.sys_dbus.systemd.is_connected and (
|
||||
not self.sys_os.available or self.sys_os.version >= AwesomeVersion("10")
|
||||
):
|
||||
features.append(HostFeature.MOUNT)
|
||||
|
||||
return features
|
||||
|
||||
async def reload(self):
|
||||
|
@ -19,6 +19,7 @@ class JobCondition(str, Enum):
|
||||
HOST_NETWORK = "host_network"
|
||||
INTERNET_HOST = "internet_host"
|
||||
INTERNET_SYSTEM = "internet_system"
|
||||
MOUNT_AVAILABLE = "mount_available"
|
||||
OS_AGENT = "os_agent"
|
||||
PLUGINS_UPDATED = "plugins_updated"
|
||||
RUNNING = "running"
|
||||
|
@ -269,6 +269,14 @@ class Job(CoreSysAttributes):
|
||||
f"'{self._method.__qualname__}' blocked from execution, was unable to update plugin(s) {', '.join(update_failures)} and all plugins must be up to date first"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.MOUNT_AVAILABLE in used_conditions
|
||||
and HostFeature.MOUNT not in self.sys_host.features
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, mounting not supported on system"
|
||||
)
|
||||
|
||||
async def _acquire_exection_limit(self) -> None:
|
||||
"""Process exection limits."""
|
||||
if self.limit not in (
|
||||
|
@ -9,7 +9,10 @@ from pathlib import PurePath
|
||||
from ..const import ATTR_NAME
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..dbus.const import UnitActiveState
|
||||
from ..exceptions import MountActivationError, MountError, MountNotFound
|
||||
from ..exceptions import MountActivationError, MountError, MountJobError, MountNotFound
|
||||
from ..host.const import HostFeature
|
||||
from ..jobs.const import JobCondition
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import SuggestionType
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.sentry import capture_exception
|
||||
@ -102,6 +105,12 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
if not self.mounts:
|
||||
return
|
||||
|
||||
if HostFeature.MOUNT not in self.sys_host.features:
|
||||
_LOGGER.error(
|
||||
"Cannot load configured mounts because mounting not supported on system!"
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.info("Initializing all user-configured mounts")
|
||||
await self._mount_errors_to_issues(
|
||||
self.mounts.copy(), [mount.load() for mount in self.mounts]
|
||||
@ -116,6 +125,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
@Job(conditions=[JobCondition.MOUNT_AVAILABLE])
|
||||
async def reload(self) -> None:
|
||||
"""Update mounts info via dbus and reload failed mounts."""
|
||||
await asyncio.wait(
|
||||
@ -153,6 +163,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
],
|
||||
)
|
||||
|
||||
@Job(conditions=[JobCondition.MOUNT_AVAILABLE], on_condition=MountJobError)
|
||||
async def create_mount(self, mount: Mount) -> None:
|
||||
"""Add/update a mount."""
|
||||
if mount.name in self._mounts:
|
||||
@ -170,6 +181,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
if mount.usage == MountUsage.MEDIA:
|
||||
await self._bind_media(mount)
|
||||
|
||||
@Job(conditions=[JobCondition.MOUNT_AVAILABLE], on_condition=MountJobError)
|
||||
async def remove_mount(self, name: str, *, retain_entry: bool = False) -> None:
|
||||
"""Remove a mount."""
|
||||
if name not in self._mounts:
|
||||
@ -192,6 +204,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
|
||||
return mount
|
||||
|
||||
@Job(conditions=[JobCondition.MOUNT_AVAILABLE], on_condition=MountJobError)
|
||||
async def reload_mount(self, name: str) -> None:
|
||||
"""Reload a mount to retry mounting with same config."""
|
||||
if name not in self._mounts:
|
||||
|
@ -158,6 +158,32 @@ async def test_api_create_dbus_error_mount_not_added(
|
||||
assert result["data"]["mounts"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("os_available", ["9.5"], indirect=True)
|
||||
async def test_api_create_mount_fails_not_supported_feature(
|
||||
api_client: TestClient,
|
||||
coresys: CoreSys,
|
||||
os_available,
|
||||
):
|
||||
"""Test creating a mount via API fails when mounting isn't a supported feature on system.."""
|
||||
resp = await api_client.post(
|
||||
"/mounts",
|
||||
json={
|
||||
"name": "backup_test",
|
||||
"type": "cifs",
|
||||
"usage": "backup",
|
||||
"server": "backup.local",
|
||||
"share": "backups",
|
||||
},
|
||||
)
|
||||
assert resp.status == 400
|
||||
result = await resp.json()
|
||||
assert result["result"] == "error"
|
||||
assert (
|
||||
result["message"]
|
||||
== "'MountManager.create_mount' blocked from execution, mounting not supported on system"
|
||||
)
|
||||
|
||||
|
||||
async def test_api_update_mount(api_client: TestClient, coresys: CoreSys, mount):
|
||||
"""Test updating a mount via API."""
|
||||
resp = await api_client.put(
|
||||
|
@ -17,8 +17,6 @@ from tests.dbus_service_mocks.agent_boards_yellow import Yellow as YellowService
|
||||
from tests.dbus_service_mocks.agent_datadisk import DataDisk as DataDiskService
|
||||
from tests.dbus_service_mocks.base import DBusServiceMock
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.fixture(name="boards_service")
|
||||
async def fixture_boards_service(
|
||||
@ -58,11 +56,12 @@ async def test_api_os_info_with_agent(api_client: TestClient, coresys: CoreSys):
|
||||
ids=["non-existent", "unavailable drive by path", "unavailable drive by id"],
|
||||
)
|
||||
async def test_api_os_datadisk_move_fail(
|
||||
api_client: TestClient, coresys: CoreSys, new_disk: str
|
||||
api_client: TestClient,
|
||||
coresys: CoreSys,
|
||||
new_disk: str,
|
||||
os_available,
|
||||
):
|
||||
"""Test datadisk move to non-existent or invalid devices."""
|
||||
coresys.os._available = True
|
||||
|
||||
resp = await api_client.post("/os/datadisk/move", json={"device": new_disk})
|
||||
result = await resp.json()
|
||||
|
||||
@ -98,11 +97,11 @@ async def test_api_os_datadisk_migrate(
|
||||
coresys: CoreSys,
|
||||
os_agent_services: dict[str, DBusServiceMock],
|
||||
new_disk: str,
|
||||
os_available,
|
||||
):
|
||||
"""Test migrating datadisk."""
|
||||
datadisk_service: DataDiskService = os_agent_services["agent_datadisk"]
|
||||
datadisk_service.ChangeDevice.calls.clear()
|
||||
coresys.os._available = True
|
||||
|
||||
with patch.object(SystemControl, "reboot") as reboot:
|
||||
resp = await api_client.post("/os/datadisk/move", json={"device": new_disk})
|
||||
|
@ -42,6 +42,7 @@ from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.docker.manager import DockerAPI
|
||||
from supervisor.docker.monitor import DockerMonitor
|
||||
from supervisor.host.logs import LogsControl
|
||||
from supervisor.os.manager import OSManager
|
||||
from supervisor.store.addon import AddonStore
|
||||
from supervisor.store.repository import Repository
|
||||
from supervisor.utils.dt import utcnow
|
||||
@ -602,3 +603,17 @@ async def capture_exception() -> Mock:
|
||||
"supervisor.utils.sentry.sentry_sdk.capture_exception"
|
||||
) as capture_exception:
|
||||
yield capture_exception
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def os_available(request: pytest.FixtureRequest) -> None:
|
||||
"""Mock os as available."""
|
||||
version = (
|
||||
AwesomeVersion(request.param)
|
||||
if hasattr(request, "param")
|
||||
else AwesomeVersion("10.0")
|
||||
)
|
||||
with patch.object(
|
||||
OSManager, "available", new=PropertyMock(return_value=True)
|
||||
), patch.object(OSManager, "version", new=PropertyMock(return_value=version)):
|
||||
yield
|
||||
|
@ -29,7 +29,11 @@ async def test_simple_task_repeat(coresys):
|
||||
trigger.append(True)
|
||||
|
||||
coresys.scheduler.register_task(test_task, 0.1, True)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0.1)
|
||||
if len(trigger) > 1:
|
||||
break
|
||||
|
||||
assert len(trigger) > 1
|
||||
|
||||
|
@ -10,7 +10,12 @@ import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import UnitActiveState
|
||||
from supervisor.exceptions import MountActivationError, MountError, MountNotFound
|
||||
from supervisor.exceptions import (
|
||||
MountActivationError,
|
||||
MountError,
|
||||
MountJobError,
|
||||
MountNotFound,
|
||||
)
|
||||
from supervisor.mounts.manager import MountManager
|
||||
from supervisor.mounts.mount import Mount
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
@ -506,3 +511,33 @@ async def test_reload_mounts(
|
||||
assert mount.state == UnitActiveState.ACTIVE
|
||||
assert mount.failed_issue not in coresys.resolution.issues
|
||||
assert not coresys.resolution.suggestions_for_issue(mount.failed_issue)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("os_available", ["9.5"], indirect=True)
|
||||
async def test_mounting_not_supported(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
os_available,
|
||||
):
|
||||
"""Test mounting not supported on system."""
|
||||
caplog.clear()
|
||||
|
||||
await coresys.mounts.load()
|
||||
assert not caplog.text
|
||||
|
||||
mount = Mount.from_dict(coresys, MEDIA_TEST_DATA)
|
||||
coresys.mounts._mounts = {"media_test": mount} # pylint: disable=protected-access
|
||||
|
||||
# Only tell the user about an issue here if they actually have mounts we couldn't load
|
||||
# This is an edge case but users can downgrade OS so its possible
|
||||
await coresys.mounts.load()
|
||||
assert "Cannot load configured mounts" in caplog.text
|
||||
|
||||
with pytest.raises(MountJobError):
|
||||
await coresys.mounts.create_mount(mount)
|
||||
|
||||
with pytest.raises(MountJobError):
|
||||
await coresys.mounts.reload_mount("media_test")
|
||||
|
||||
with pytest.raises(MountJobError):
|
||||
await coresys.mounts.remove_mount("media_test")
|
||||
|
@ -22,8 +22,6 @@ from tests.dbus_service_mocks.udisks2_partition_table import (
|
||||
PartitionTable as PartitionTableService,
|
||||
)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def add_unusable_drive(
|
||||
@ -70,10 +68,8 @@ async def tests_datadisk_current(coresys: CoreSys):
|
||||
["/dev/sdaaaa", "/dev/mmcblk1", "Generic-Flash-Disk-61BCDDB6"],
|
||||
ids=["non-existent", "unavailable drive by path", "unavailable drive by id"],
|
||||
)
|
||||
async def test_datadisk_move_fail(coresys: CoreSys, new_disk: str):
|
||||
async def test_datadisk_move_fail(coresys: CoreSys, new_disk: str, os_available):
|
||||
"""Test datadisk move to non-existent or invalid devices."""
|
||||
coresys.os._available = True
|
||||
|
||||
with pytest.raises(
|
||||
HassOSDataDiskError, match=f"'{new_disk}' not a valid data disk target!"
|
||||
):
|
||||
@ -112,13 +108,13 @@ async def test_datadisk_migrate(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
new_disk: str,
|
||||
os_available,
|
||||
):
|
||||
"""Test migrating data disk."""
|
||||
datadisk_service: DataDiskService = all_dbus_services["agent_datadisk"]
|
||||
datadisk_service.ChangeDevice.calls.clear()
|
||||
logind_service: LogindService = all_dbus_services["logind"]
|
||||
logind_service.Reboot.calls.clear()
|
||||
coresys.os._available = True
|
||||
|
||||
with patch.object(Core, "shutdown") as shutdown:
|
||||
await coresys.os.datadisk.migrate_disk(new_disk)
|
||||
@ -137,6 +133,7 @@ async def test_datadisk_migrate_mark_data_move(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
new_disk: str,
|
||||
os_available,
|
||||
):
|
||||
"""Test migrating data disk with os agent 1.5.0 or later."""
|
||||
datadisk_service: DataDiskService = all_dbus_services["agent_datadisk"]
|
||||
@ -155,7 +152,6 @@ async def test_datadisk_migrate_mark_data_move(
|
||||
|
||||
all_dbus_services["os_agent"].emit_properties_changed({"Version": "1.5.0"})
|
||||
await all_dbus_services["os_agent"].ping()
|
||||
coresys.os._available = True
|
||||
|
||||
with patch.object(Core, "shutdown") as shutdown:
|
||||
await coresys.os.datadisk.migrate_disk(new_disk)
|
||||
@ -181,6 +177,7 @@ async def test_datadisk_migrate_mark_data_move(
|
||||
async def test_datadisk_migrate_too_small(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
os_available,
|
||||
):
|
||||
"""Test migration stops and exits if new partition is too small."""
|
||||
datadisk_service: DataDiskService = all_dbus_services["agent_datadisk"]
|
||||
@ -198,7 +195,6 @@ async def test_datadisk_migrate_too_small(
|
||||
|
||||
all_dbus_services["os_agent"].emit_properties_changed({"Version": "1.5.0"})
|
||||
await all_dbus_services["os_agent"].ping()
|
||||
coresys.os._available = True
|
||||
|
||||
with pytest.raises(
|
||||
HassOSDataDiskError,
|
||||
@ -214,6 +210,7 @@ async def test_datadisk_migrate_too_small(
|
||||
async def test_datadisk_migrate_multiple_external_data_disks(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
os_available,
|
||||
):
|
||||
"""Test migration stops when another hassos-data-external partition detected."""
|
||||
datadisk_service: DataDiskService = all_dbus_services["agent_datadisk"]
|
||||
@ -226,7 +223,6 @@ async def test_datadisk_migrate_multiple_external_data_disks(
|
||||
sdb1_filesystem_service.fixture = replace(
|
||||
sdb1_filesystem_service.fixture, MountPoints=[]
|
||||
)
|
||||
coresys.os._available = True
|
||||
|
||||
with pytest.raises(
|
||||
HassOSDataDiskError,
|
||||
@ -241,6 +237,7 @@ async def test_datadisk_migrate_multiple_external_data_disks(
|
||||
async def test_datadisk_migrate_between_external_renames(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||
os_available,
|
||||
):
|
||||
"""Test migration from one external data disk to another renames the original."""
|
||||
sdb1_partition_service: PartitionService = all_dbus_services["udisks2_partition"][
|
||||
@ -266,7 +263,6 @@ async def test_datadisk_migrate_between_external_renames(
|
||||
|
||||
all_dbus_services["os_agent"].emit_properties_changed({"Version": "1.5.0"})
|
||||
await all_dbus_services["os_agent"].ping()
|
||||
coresys.os._available = True
|
||||
|
||||
await coresys.os.datadisk.migrate_disk("Generic-Flash-Disk-61BCDDB6")
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Test evaluation base."""
|
||||
# pylint: disable=import-error,protected-access
|
||||
# pylint: disable=import-error
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreState
|
||||
@ -24,7 +24,6 @@ async def test_evaluation(coresys: CoreSys):
|
||||
coresys.resolution.unsupported.clear()
|
||||
|
||||
coresys.docker.info.cgroup = CGROUP_V2_VERSION
|
||||
coresys.os._available = False
|
||||
await cgroup_version()
|
||||
assert cgroup_version.reason in coresys.resolution.unsupported
|
||||
coresys.resolution.unsupported.clear()
|
||||
@ -34,12 +33,11 @@ async def test_evaluation(coresys: CoreSys):
|
||||
assert cgroup_version.reason not in coresys.resolution.unsupported
|
||||
|
||||
|
||||
async def test_evaluation_os_available(coresys: CoreSys):
|
||||
async def test_evaluation_os_available(coresys: CoreSys, os_available):
|
||||
"""Test evaluation with OS available."""
|
||||
cgroup_version = EvaluateCGroupVersion(coresys)
|
||||
coresys.core.state = CoreState.SETUP
|
||||
|
||||
coresys.os._available = True
|
||||
coresys.docker.info.cgroup = CGROUP_V2_VERSION
|
||||
await cgroup_version()
|
||||
assert cgroup_version.reason not in coresys.resolution.unsupported
|
||||
|
Loading…
x
Reference in New Issue
Block a user