Skip to content

Commit

Permalink
Adopt a disabled data disk
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 committed Apr 10, 2024
1 parent a0735f3 commit 6c2a597
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 49 deletions.
1 change: 1 addition & 0 deletions supervisor/os/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Constants for OS."""

FILESYSTEM_LABEL_DATA_DISK = "hassos-data"
FILESYSTEM_LABEL_DISABLED_DATA_DISK = "hassos-data-dis"
FILESYSTEM_LABEL_OLD_DATA_DISK = "hassos-data-old"
PARTITION_NAME_EXTERNAL_DATA_DISK = "hassos-data-external"
PARTITION_NAME_OLD_EXTERNAL_DATA_DISK = "hassos-data-external-old"
63 changes: 63 additions & 0 deletions supervisor/resolution/checks/disabled_data_disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Helpers to check for a disabled data disk."""

from pathlib import Path

from ...const import CoreState
from ...coresys import CoreSys
from ...dbus.udisks2.block import UDisks2Block
from ...dbus.udisks2.data import DeviceSpecification
from ...os.const import FILESYSTEM_LABEL_DISABLED_DATA_DISK
from ..const import ContextType, IssueType, SuggestionType
from .base import CheckBase


def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDisabledDataDisk(coresys)


class CheckDisabledDataDisk(CheckBase):
"""CheckDisabledDataDisk class for check."""

async def run_check(self) -> None:
"""Run check if not affected by issue."""
for block_device in self.sys_dbus.udisks2.block_devices:
if self._is_disabled_data_disk(block_device):
self.sys_resolution.create_issue(
IssueType.DISABLED_DATA_DISK,
ContextType.SYSTEM,
reference=block_device.device.as_posix(),
suggestions=[
SuggestionType.RENAME_DATA_DISK,
SuggestionType.ADOPT_DATA_DISK,
],
)

async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
resolved = await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
)
return resolved and self._is_disabled_data_disk(resolved[0])

def _is_disabled_data_disk(self, block_device: UDisks2Block) -> bool:
"""Return true if filesystem block device has name indicating it was disabled by OS."""
return (
block_device.filesystem
and block_device.id_label == FILESYSTEM_LABEL_DISABLED_DATA_DISK
)

@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DISABLED_DATA_DISK

@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM

@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.RUNNING, CoreState.SETUP]
1 change: 1 addition & 0 deletions supervisor/resolution/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class IssueType(StrEnum):
CORRUPT_DOCKER = "corrupt_docker"
CORRUPT_REPOSITORY = "corrupt_repository"
CORRUPT_FILESYSTEM = "corrupt_filesystem"
DISABLED_DATA_DISK = "disabled_data_disk"
DNS_LOOP = "dns_loop"
DNS_SERVER_FAILED = "dns_server_failed"
DNS_SERVER_IPV6_ERROR = "dns_server_ipv6_error"
Expand Down
32 changes: 25 additions & 7 deletions supervisor/resolution/fixups/system_adopt_data_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ...coresys import CoreSys
from ...dbus.udisks2.data import DeviceSpecification
from ...exceptions import DBusError, HostError, ResolutionFixupError
from ...os.const import FILESYSTEM_LABEL_OLD_DATA_DISK
from ...os.const import FILESYSTEM_LABEL_DATA_DISK, FILESYSTEM_LABEL_OLD_DATA_DISK
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase

Expand All @@ -23,8 +23,10 @@ class FixupSystemAdoptDataDisk(FixupBase):

async def process_fixup(self, reference: str | None = None) -> None:
"""Initialize the fixup class."""
if not await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
if not (
new_resolved := await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
)
):
_LOGGER.info(
"Data disk at %s with name conflict was removed, skipping adopt",
Expand All @@ -36,24 +38,40 @@ async def process_fixup(self, reference: str | None = None) -> None:
if (
not current
or not (
resolved := await self.sys_dbus.udisks2.resolve_device(
current_resolved := await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=current)
)
)
or not resolved[0].filesystem
or not current_resolved[0].filesystem
):
raise ResolutionFixupError(
"Cannot resolve current data disk for rename", _LOGGER.error
)

if new_resolved[0].id_label != FILESYSTEM_LABEL_DATA_DISK:
_LOGGER.info(
"Renaming disabled data disk at %s to %s to activate it",
reference,
FILESYSTEM_LABEL_DATA_DISK,
)
try:
await new_resolved[0].filesystem.set_label(FILESYSTEM_LABEL_DATA_DISK)
except DBusError as err:
raise ResolutionFixupError(
f"Could not rename filesystem at {reference}: {err!s}",
_LOGGER.error,
) from err

_LOGGER.info(
"Renaming current data disk at %s to %s so new data disk at %s becomes primary ",
self.sys_dbus.agent.datadisk.current_device,
FILESYSTEM_LABEL_OLD_DATA_DISK,
reference,
)
try:
await resolved[0].filesystem.set_label(FILESYSTEM_LABEL_OLD_DATA_DISK)
await current_resolved[0].filesystem.set_label(
FILESYSTEM_LABEL_OLD_DATA_DISK
)
except DBusError as err:
raise ResolutionFixupError(
f"Could not rename filesystem at {current.as_posix()}: {err!s}",
Expand Down Expand Up @@ -87,4 +105,4 @@ def context(self) -> ContextType:
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.MULTIPLE_DATA_DISKS]
return [IssueType.DISABLED_DATA_DISK, IssueType.MULTIPLE_DATA_DISKS]
2 changes: 1 addition & 1 deletion supervisor/resolution/fixups/system_rename_data_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ def context(self) -> ContextType:
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.MULTIPLE_DATA_DISKS]
return [IssueType.DISABLED_DATA_DISK, IssueType.MULTIPLE_DATA_DISKS]
6 changes: 5 additions & 1 deletion tests/dbus/udisks2/test_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,9 @@ async def test_set_label(
filesystem_sda1_service.SetLabel.calls.clear()
await sda1.set_label("test")
assert filesystem_sda1_service.SetLabel.calls == [
("test", {"auth.no_user_interaction": Variant("b", True)})
(
"/org/freedesktop/UDisks2/block_devices/sda1",
"test",
{"auth.no_user_interaction": Variant("b", True)},
)
]
55 changes: 29 additions & 26 deletions tests/dbus_service_mocks/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,6 @@
from dbus_fast.service import ServiceInterface, method


def dbus_method(name: str = None, disabled: bool = False):
"""Make DBus method with call tracking.
Identical to dbus_fast.service.method wrapper except all calls to it are tracked.
Can then test that methods with no output were called or the right arguments were
used if the output is static.
"""
orig_decorator = method(name=name, disabled=disabled)

@no_type_check_decorator
def decorator(func):
calls: list[list[Any]] = []

@wraps(func)
def track_calls(self, *args):
calls.append(args)
return func(self, *args)

wrapped = orig_decorator(track_calls)
wrapped.__dict__["calls"] = calls

return wrapped

return decorator


class DBusServiceMock(ServiceInterface):
"""Base dbus service mock."""

Expand Down Expand Up @@ -66,3 +40,32 @@ async def ping(self, *, sleep: bool = True):
# So in general we sleep(0) after to clear the new task
if sleep:
await asyncio.sleep(0)


def dbus_method(name: str = None, disabled: bool = False, track_obj_path: bool = False):
"""Make DBus method with call tracking.
Identical to dbus_fast.service.method wrapper except all calls to it are tracked.
Can then test that methods with no output were called or the right arguments were
used if the output is static.
"""
orig_decorator = method(name=name, disabled=disabled)

@no_type_check_decorator
def decorator(func):
calls: list[list[Any]] = []

@wraps(func)
def track_calls(self: DBusServiceMock, *args):
if track_obj_path:
calls.append((self.object_path, *args))
else:
calls.append(args)
return func(self, *args)

wrapped = orig_decorator(track_calls)
wrapped.__dict__["calls"] = calls

return wrapped

return decorator
2 changes: 1 addition & 1 deletion tests/dbus_service_mocks/udisks2_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def Size(self) -> "t":
"""Get Size."""
return self.fixture.Size

@dbus_method()
@dbus_method(track_obj_path=True)
def SetLabel(self, label: "s", options: "a{sv}") -> None:
"""Do SetLabel method."""

Expand Down
8 changes: 7 additions & 1 deletion tests/dbus_service_mocks/udisks2_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class UDisks2Manager(DBusServiceMock):
"/org/freedesktop/UDisks2/block_devices/sdb1",
"/org/freedesktop/UDisks2/block_devices/zram1",
]
resolved_devices = ["/org/freedesktop/UDisks2/block_devices/sda1"]
resolved_devices: list[list[str]] | list[str] = [
"/org/freedesktop/UDisks2/block_devices/sda1"
]

@dbus_property(access=PropertyAccess.READ)
def Version(self) -> "s":
Expand Down Expand Up @@ -98,4 +100,8 @@ def GetBlockDevices(self, options: "a{sv}") -> "ao":
@dbus_method()
def ResolveDevice(self, devspec: "a{sv}", options: "a{sv}") -> "ao":
"""Do ResolveDevice method."""
if len(self.resolved_devices) > 0 and isinstance(
self.resolved_devices[0], list
):
return self.resolved_devices.pop(0)
return self.resolved_devices
99 changes: 99 additions & 0 deletions tests/resolution/check/test_check_disabled_data_disk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""Test check for disabled data disk."""
# pylint: disable=import-error
from dataclasses import replace
from unittest.mock import patch

import pytest

from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.disabled_data_disk import CheckDisabledDataDisk
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion

from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.udisks2_block import Block as BlockService


@pytest.fixture(name="sda1_block_service")
async def fixture_sda1_block_service(
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> BlockService:
"""Return sda1 block service."""
yield udisks2_services["udisks2_block"][
"/org/freedesktop/UDisks2/block_devices/sda1"
]


async def test_base(coresys: CoreSys):
"""Test check basics."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
assert disabled_data_disk.slug == "disabled_data_disk"
assert disabled_data_disk.enabled


async def test_check(coresys: CoreSys, sda1_block_service: BlockService):
"""Test check."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
coresys.core.state = CoreState.RUNNING

await disabled_data_disk.run_check()

assert len(coresys.resolution.issues) == 0
assert len(coresys.resolution.suggestions) == 0

sda1_block_service.emit_properties_changed({"IdLabel": "hassos-data-dis"})
await sda1_block_service.ping()

await disabled_data_disk.run_check()

assert coresys.resolution.issues == [
Issue(IssueType.DISABLED_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1")
]
assert coresys.resolution.suggestions == [
Suggestion(
SuggestionType.RENAME_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
),
Suggestion(
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
),
]


async def test_approve(coresys: CoreSys, sda1_block_service: BlockService):
"""Test approve."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
coresys.core.state = CoreState.RUNNING

assert not await disabled_data_disk.approve_check(reference="/dev/sda1")

sda1_block_service.fixture = replace(
sda1_block_service.fixture, IdLabel="hassos-data-dis"
)

assert await disabled_data_disk.approve_check(reference="/dev/sda1")


async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
should_run = disabled_data_disk.states
should_not_run = [state for state in CoreState if state not in should_run]
assert len(should_run) != 0
assert len(should_not_run) != 0

with patch(
"supervisor.resolution.checks.disabled_data_disk.CheckDisabledDataDisk.run_check",
return_value=None,
) as check:
for state in should_run:
coresys.core.state = state
await disabled_data_disk()
check.assert_called_once()
check.reset_mock()

for state in should_not_run:
coresys.core.state = state
await disabled_data_disk()
check.assert_not_called()
check.reset_mock()
Loading

0 comments on commit 6c2a597

Please sign in to comment.