Skip to content

Commit 86f70cc

Browse files
committed
Add unsupported reason os_version and evaluation
1 parent 3b1b03c commit 86f70cc

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

supervisor/resolution/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class UnsupportedReason(StrEnum):
5858
SYSTEMD_JOURNAL = "systemd_journal"
5959
SYSTEMD_RESOLVED = "systemd_resolved"
6060
VIRTUALIZATION_IMAGE = "virtualization_image"
61+
OS_VERSION = "os_version"
6162

6263

6364
class UnhealthyReason(StrEnum):
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Evaluation class for OS version."""
2+
3+
from awesomeversion import AwesomeVersion, AwesomeVersionException
4+
5+
from ...const import CoreState
6+
from ...coresys import CoreSys
7+
from ..const import UnsupportedReason
8+
from .base import EvaluateBase
9+
10+
11+
def setup(coresys: CoreSys) -> EvaluateBase:
12+
"""Initialize evaluation-setup function."""
13+
return EvaluateOSVersion(coresys)
14+
15+
16+
class EvaluateOSVersion(EvaluateBase):
17+
"""Evaluate the OS version."""
18+
19+
@property
20+
def reason(self) -> UnsupportedReason:
21+
"""Return a UnsupportedReason enum."""
22+
return UnsupportedReason.OS_VERSION
23+
24+
@property
25+
def on_failure(self) -> str:
26+
"""Return a string that is printed when self.evaluate is True."""
27+
return f"OS version '{self.sys_os.version}' is more than 4 versions behind the latest '{self.sys_os.latest_version}'!"
28+
29+
@property
30+
def states(self) -> list[CoreState]:
31+
"""Return a list of valid states when this evaluation can run."""
32+
# Technically there's no reason to run this after STARTUP as update requires
33+
# a reboot. But if network is down we won't have latest version info then.
34+
return [CoreState.RUNNING, CoreState.STARTUP]
35+
36+
async def evaluate(self) -> bool:
37+
"""Run evaluation."""
38+
if (
39+
not self.sys_os.available
40+
or not (current := self.sys_os.version)
41+
or not (latest := self.sys_os.latest_version)
42+
or not latest.major
43+
):
44+
return False
45+
46+
# If current is more than 4 major versions behind latest, mark as unsupported
47+
last_supported_version = AwesomeVersion(f"{int(latest.major) - 4}.0")
48+
try:
49+
return current < last_supported_version
50+
except AwesomeVersionException:
51+
return False
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Test OS Version evaluation."""
2+
3+
from unittest.mock import PropertyMock, patch
4+
5+
from awesomeversion import AwesomeVersion
6+
import pytest
7+
8+
from supervisor.const import CoreState
9+
from supervisor.coresys import CoreSys
10+
from supervisor.os.manager import OSManager
11+
from supervisor.resolution.evaluations.os_version import EvaluateOSVersion
12+
13+
14+
@pytest.mark.parametrize(
15+
"current,latest,expected",
16+
[
17+
("10.0", "15.0", True), # 5 major behind, should be unsupported
18+
("10.0", "14.0", False), # 4 major behind, should be supported
19+
("10.2", "11.0", False), # 1 major behind, supported
20+
("10.4", "10.5", False), # same major, supported
21+
("10.5", "10.5", False), # up to date, supported
22+
("10.5", "10.6", False), # same major, supported
23+
("10.0", "13.3", False), # 3 major behind, supported
24+
(None, "15.0", False), # No current version info, check skipped
25+
("2.0", None, False), # No latest version info, check skipped
26+
(
27+
"9ccda431973acf17e4221850b08f3280b723df8d",
28+
"15.0",
29+
False,
30+
), # Dev setup running on a commit hash, check skipped
31+
],
32+
)
33+
@pytest.mark.usefixtures("os_available")
34+
async def test_os_version_evaluation(
35+
coresys: CoreSys, current: str | None, latest: str | None, expected: bool
36+
):
37+
"""Test evaluation logic on versions."""
38+
evaluation = EvaluateOSVersion(coresys)
39+
await coresys.core.set_state(CoreState.RUNNING)
40+
with (
41+
patch.object(
42+
OSManager,
43+
"version",
44+
new=PropertyMock(return_value=current and AwesomeVersion(current)),
45+
),
46+
patch.object(
47+
OSManager,
48+
"latest_version",
49+
new=PropertyMock(return_value=latest and AwesomeVersion(latest)),
50+
),
51+
):
52+
assert evaluation.reason not in coresys.resolution.unsupported
53+
await evaluation()
54+
assert (evaluation.reason in coresys.resolution.unsupported) is expected
55+
56+
57+
async def test_did_run(coresys: CoreSys):
58+
"""Test that the evaluation ran as expected."""
59+
evaluation = EvaluateOSVersion(coresys)
60+
should_run = evaluation.states
61+
should_not_run = [state for state in CoreState if state not in should_run]
62+
assert len(should_run) != 0
63+
assert len(should_not_run) != 0
64+
65+
with patch(
66+
"supervisor.resolution.evaluations.os_version.EvaluateOSVersion.evaluate",
67+
return_value=None,
68+
) as evaluate:
69+
for state in should_run:
70+
await coresys.core.set_state(state)
71+
await evaluation()
72+
evaluate.assert_called_once()
73+
evaluate.reset_mock()
74+
75+
for state in should_not_run:
76+
await coresys.core.set_state(state)
77+
await evaluation()
78+
evaluate.assert_not_called()
79+
evaluate.reset_mock()

0 commit comments

Comments
 (0)