Skip to content

Commit 8a1e6b0

Browse files
mdegat01agners
andauthored
Add unsupported reason os_version and evaluation (#6041)
* Add unsupported reason os_version and evaluation * Order enum and add tests * Apply suggestions from code review * Apply suggestions from code review --------- Co-authored-by: Stefan Agner <[email protected]>
1 parent f150d1b commit 8a1e6b0

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-0
lines changed

supervisor/resolution/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class UnsupportedReason(StrEnum):
4949
NETWORK_MANAGER = "network_manager"
5050
OS = "os"
5151
OS_AGENT = "os_agent"
52+
OS_VERSION = "os_version"
5253
PRIVILEGED = "privileged"
5354
RESTART_POLICY = "restart_policy"
5455
SOFTWARE = "software"
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 True
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
("10.2.dev20240321", "15.0", True), # 5 major behind, dev version, unsupported
25+
("10.2.dev20240321", "13.0", False), # 3 major behind, dev version, supported
26+
("10.2.rc2", "15.0", True), # 5 major behind, rc version, unsupported
27+
("10.2.rc2", "13.0", False), # 3 major behind, rc version, supported
28+
(None, "15.0", False), # No current version info, check skipped
29+
("2.0", None, False), # No latest version info, check skipped
30+
(
31+
"9ccda431973acf17e4221850b08f3280b723df8d",
32+
"15.0",
33+
True,
34+
), # Dev setup running on a commit hash, check skipped
35+
],
36+
)
37+
@pytest.mark.usefixtures("os_available")
38+
async def test_os_version_evaluation(
39+
coresys: CoreSys, current: str | None, latest: str | None, expected: bool
40+
):
41+
"""Test evaluation logic on versions."""
42+
evaluation = EvaluateOSVersion(coresys)
43+
await coresys.core.set_state(CoreState.RUNNING)
44+
with (
45+
patch.object(
46+
OSManager,
47+
"version",
48+
new=PropertyMock(return_value=current and AwesomeVersion(current)),
49+
),
50+
patch.object(
51+
OSManager,
52+
"latest_version",
53+
new=PropertyMock(return_value=latest and AwesomeVersion(latest)),
54+
),
55+
):
56+
assert evaluation.reason not in coresys.resolution.unsupported
57+
await evaluation()
58+
assert (evaluation.reason in coresys.resolution.unsupported) is expected
59+
60+
61+
async def test_did_run(coresys: CoreSys):
62+
"""Test that the evaluation ran as expected."""
63+
evaluation = EvaluateOSVersion(coresys)
64+
should_run = evaluation.states
65+
should_not_run = [state for state in CoreState if state not in should_run]
66+
assert len(should_run) != 0
67+
assert len(should_not_run) != 0
68+
69+
with patch(
70+
"supervisor.resolution.evaluations.os_version.EvaluateOSVersion.evaluate",
71+
return_value=None,
72+
) as evaluate:
73+
for state in should_run:
74+
await coresys.core.set_state(state)
75+
await evaluation()
76+
evaluate.assert_called_once()
77+
evaluate.reset_mock()
78+
79+
for state in should_not_run:
80+
await coresys.core.set_state(state)
81+
await evaluation()
82+
evaluate.assert_not_called()
83+
evaluate.reset_mock()

0 commit comments

Comments
 (0)