Skip to content

Commit fdde95d

Browse files
authored
Add an issue for disk lifetime >90% (#6069)
1 parent 65e5a36 commit fdde95d

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Helpers to check disk lifetime issues."""
2+
3+
from ...const import CoreState
4+
from ...coresys import CoreSys
5+
from ..const import ContextType, IssueType
6+
from .base import CheckBase
7+
8+
9+
def setup(coresys: CoreSys) -> CheckBase:
10+
"""Check setup function."""
11+
return CheckDiskLifetime(coresys)
12+
13+
14+
class CheckDiskLifetime(CheckBase):
15+
"""Storage class for check."""
16+
17+
async def run_check(self) -> None:
18+
"""Run check if not affected by issue."""
19+
if await self.approve_check():
20+
self.sys_resolution.create_issue(
21+
IssueType.DISK_LIFETIME, ContextType.SYSTEM
22+
)
23+
24+
async def approve_check(self, reference: str | None = None) -> bool:
25+
"""Approve check if it is affected by issue."""
26+
# Get the current data disk device
27+
if not self.sys_dbus.agent.datadisk.current_device:
28+
return False
29+
30+
# Check disk lifetime
31+
lifetime = await self.sys_hardware.disk.get_disk_life_time(
32+
self.sys_dbus.agent.datadisk.current_device
33+
)
34+
35+
# Issue still exists if lifetime is >= 90%
36+
return lifetime is not None and lifetime >= 90
37+
38+
@property
39+
def issue(self) -> IssueType:
40+
"""Return a IssueType enum."""
41+
return IssueType.DISK_LIFETIME
42+
43+
@property
44+
def context(self) -> ContextType:
45+
"""Return a ContextType enum."""
46+
return ContextType.SYSTEM
47+
48+
@property
49+
def states(self) -> list[CoreState]:
50+
"""Return a list of valid states when this check can run."""
51+
return [CoreState.RUNNING, CoreState.STARTUP]

supervisor/resolution/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class IssueType(StrEnum):
8484
DETACHED_ADDON_REMOVED = "detached_addon_removed"
8585
DEVICE_ACCESS_MISSING = "device_access_missing"
8686
DISABLED_DATA_DISK = "disabled_data_disk"
87+
DISK_LIFETIME = "disk_lifetime"
8788
DNS_LOOP = "dns_loop"
8889
DUPLICATE_OS_INSTALLATION = "duplicate_os_installation"
8990
DNS_SERVER_FAILED = "dns_server_failed"
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Test check disk lifetime fixup."""
2+
3+
# pylint: disable=import-error,protected-access
4+
from unittest.mock import PropertyMock, patch
5+
6+
import pytest
7+
8+
from supervisor.const import CoreState
9+
from supervisor.coresys import CoreSys
10+
from supervisor.resolution.checks.disk_lifetime import CheckDiskLifetime
11+
from supervisor.resolution.const import ContextType, IssueType
12+
from supervisor.resolution.data import Issue
13+
14+
15+
async def test_base(coresys: CoreSys):
16+
"""Test check basics."""
17+
disk_lifetime = CheckDiskLifetime(coresys)
18+
assert disk_lifetime.slug == "disk_lifetime"
19+
assert disk_lifetime.enabled
20+
21+
22+
async def test_check_no_data_disk(coresys: CoreSys):
23+
"""Test check when no data disk is available."""
24+
disk_lifetime = CheckDiskLifetime(coresys)
25+
await coresys.core.set_state(CoreState.RUNNING)
26+
27+
# Mock no data disk
28+
with patch.object(
29+
type(coresys.dbus.agent.datadisk),
30+
"current_device",
31+
new=PropertyMock(return_value=None),
32+
):
33+
await disk_lifetime()
34+
35+
assert len(coresys.resolution.issues) == 0
36+
37+
38+
@pytest.mark.parametrize(
39+
("lifetime", "has_issue"),
40+
[(0.0, False), (85.0, False), (90.0, True), (95.0, True), (None, False)],
41+
)
42+
async def test_check_lifetime_threshold(
43+
coresys: CoreSys, lifetime: float | None, has_issue: bool
44+
):
45+
"""Test check when disk lifetime at thresholds."""
46+
disk_lifetime = CheckDiskLifetime(coresys)
47+
await coresys.core.set_state(CoreState.RUNNING)
48+
49+
# Mock data disk with lifetime
50+
with (
51+
patch.object(
52+
type(coresys.dbus.agent.datadisk),
53+
"current_device",
54+
new=PropertyMock(return_value="/dev/sda1"),
55+
),
56+
patch.object(
57+
coresys.hardware.disk,
58+
"get_disk_life_time",
59+
return_value=lifetime,
60+
),
61+
):
62+
await disk_lifetime()
63+
64+
assert (
65+
Issue(IssueType.DISK_LIFETIME, ContextType.SYSTEM) in coresys.resolution.issues
66+
) is has_issue
67+
68+
69+
async def test_approve_no_data_disk(coresys: CoreSys):
70+
"""Test approve when no data disk is available."""
71+
disk_lifetime = CheckDiskLifetime(coresys)
72+
73+
# Mock no data disk
74+
with patch.object(
75+
type(coresys.dbus.agent.datadisk),
76+
"current_device",
77+
new=PropertyMock(return_value=None),
78+
):
79+
assert not await disk_lifetime.approve_check()
80+
81+
82+
@pytest.mark.parametrize(
83+
("lifetime", "approved"),
84+
[(0.0, False), (85.0, False), (90.0, True), (95.0, True), (None, False)],
85+
)
86+
async def test_approve_check_lifetime_threshold(
87+
coresys: CoreSys, lifetime: float | None, approved: bool
88+
):
89+
"""Test approve check when disk lifetime at thresholds."""
90+
disk_lifetime = CheckDiskLifetime(coresys)
91+
await coresys.core.set_state(CoreState.RUNNING)
92+
93+
# Mock data disk with lifetime
94+
with (
95+
patch.object(
96+
type(coresys.dbus.agent.datadisk),
97+
"current_device",
98+
new=PropertyMock(return_value="/dev/sda1"),
99+
),
100+
patch.object(
101+
coresys.hardware.disk,
102+
"get_disk_life_time",
103+
return_value=lifetime,
104+
),
105+
):
106+
assert await disk_lifetime.approve_check() is approved
107+
108+
109+
async def test_did_run(coresys: CoreSys):
110+
"""Test that the check ran as expected."""
111+
disk_lifetime = CheckDiskLifetime(coresys)
112+
should_run = disk_lifetime.states
113+
should_not_run = [state for state in CoreState if state not in should_run]
114+
assert len(should_run) != 0
115+
assert len(should_not_run) != 0
116+
117+
with patch(
118+
"supervisor.resolution.checks.disk_lifetime.CheckDiskLifetime.run_check",
119+
return_value=None,
120+
) as check:
121+
for state in should_run:
122+
await coresys.core.set_state(state)
123+
await disk_lifetime()
124+
check.assert_called_once()
125+
check.reset_mock()
126+
127+
for state in should_not_run:
128+
await coresys.core.set_state(state)
129+
await disk_lifetime()
130+
check.assert_not_called()
131+
check.reset_mock()

0 commit comments

Comments
 (0)