Skip to content

Commit c712d3c

Browse files
agnersCopilot
andauthored
Check Core version and raise unsupported if older than 2 years (#6148)
* Check Core version and raise unsupported if older than 2 years Check the currently installed Core version relative to the current date, and if its older than 2 years, mark the system unsupported. Also add a Job condition to prevent automatic refreshing of the update information in this case. * Handle landing page correctly * Handle non-parseable versions gracefully Also align handling between OS and Core version evaluations. * Extend and fix test coverage * Improve Job condition error * Fix pytest * Block execution of fetch_data and store reload jobs Block execution of fetch_data and store reload jobs if the core version is unsupported. This essentially freezes the installation until the user takes action and updates the Core version to a supported one. * Use latest known Core version as reference Instead of using current date to determine if Core version is more than 2 years old, use the latest known Core version as reference point and check if current version is more than 24 releases behind. This is crucial because when update information refresh is disabled due to unsupported Core version, using date would create a permanent unsupported state. Even if users update to the last known version in 4+ years, the system would remain unsupported. By using latest known version as reference, updating Core to the last known version makes the system supported again, allowing update information refresh to resume. This ensures users can always escape the unsupported state by updating to the last known Core version, maintaining the update refresh cycle. * Improve version comparision logic * Use Home Assistant Core instead of just Core Avoid any ambiguity in what is exactly outdated/unsupported by using Home Assistant Core instead of just Core. * Sort const alphabetically * Update tests/resolution/evaluation/test_evaluate_home_assistant_core_version.py Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 46fc5c8 commit c712d3c

File tree

10 files changed

+319
-5
lines changed

10 files changed

+319
-5
lines changed

supervisor/jobs/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class JobCondition(StrEnum):
2424
FROZEN = "frozen"
2525
HAOS = "haos"
2626
HEALTHY = "healthy"
27+
HOME_ASSISTANT_CORE_SUPPORTED = "home_assistant_core_supported"
2728
HOST_NETWORK = "host_network"
2829
INTERNET_HOST = "internet_host"
2930
INTERNET_SYSTEM = "internet_system"

supervisor/jobs/decorator.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,15 @@ async def check_conditions(
404404
f"'{method_name}' blocked from execution, unsupported OS version"
405405
)
406406

407+
if (
408+
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED in used_conditions
409+
and UnsupportedReason.HOME_ASSISTANT_CORE_VERSION
410+
in coresys.sys_resolution.unsupported
411+
):
412+
raise JobConditionException(
413+
f"'{method_name}' blocked from execution, unsupported Home Assistant Core version"
414+
)
415+
407416
if (
408417
JobCondition.HOST_NETWORK in used_conditions
409418
and not coresys.sys_dbus.network.is_connected

supervisor/misc/tasks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,11 @@ async def _watchdog_addon_application(self):
358358

359359
@Job(
360360
name="tasks_reload_store",
361-
conditions=[JobCondition.SUPERVISOR_UPDATED, JobCondition.OS_SUPPORTED],
361+
conditions=[
362+
JobCondition.SUPERVISOR_UPDATED,
363+
JobCondition.OS_SUPPORTED,
364+
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
365+
],
362366
)
363367
async def _reload_store(self) -> None:
364368
"""Reload store and check for addon updates."""

supervisor/resolution/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class UnsupportedReason(StrEnum):
4444
DNS_SERVER = "dns_server"
4545
DOCKER_CONFIGURATION = "docker_configuration"
4646
DOCKER_VERSION = "docker_version"
47+
HOME_ASSISTANT_CORE_VERSION = "home_assistant_core_version"
4748
JOB_CONDITIONS = "job_conditions"
4849
LXC = "lxc"
4950
NETWORK_MANAGER = "network_manager"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Evaluation class for Core version."""
2+
3+
import logging
4+
5+
from awesomeversion import (
6+
AwesomeVersion,
7+
AwesomeVersionException,
8+
AwesomeVersionStrategy,
9+
)
10+
11+
from ...const import CoreState
12+
from ...coresys import CoreSys
13+
from ...homeassistant.const import LANDINGPAGE
14+
from ..const import UnsupportedReason
15+
from .base import EvaluateBase
16+
17+
_LOGGER: logging.Logger = logging.getLogger(__name__)
18+
19+
20+
def setup(coresys: CoreSys) -> EvaluateBase:
21+
"""Initialize evaluation-setup function."""
22+
return EvaluateHomeAssistantCoreVersion(coresys)
23+
24+
25+
class EvaluateHomeAssistantCoreVersion(EvaluateBase):
26+
"""Evaluate the Home Assistant Core version."""
27+
28+
@property
29+
def reason(self) -> UnsupportedReason:
30+
"""Return a UnsupportedReason enum."""
31+
return UnsupportedReason.HOME_ASSISTANT_CORE_VERSION
32+
33+
@property
34+
def on_failure(self) -> str:
35+
"""Return a string that is printed when self.evaluate is True."""
36+
return f"Home Assistant Core version '{self.sys_homeassistant.version}' is more than 2 years old!"
37+
38+
@property
39+
def states(self) -> list[CoreState]:
40+
"""Return a list of valid states when this evaluation can run."""
41+
return [CoreState.RUNNING, CoreState.SETUP]
42+
43+
async def evaluate(self) -> bool:
44+
"""Run evaluation."""
45+
if not (current := self.sys_homeassistant.version) or not (
46+
latest := self.sys_homeassistant.latest_version
47+
):
48+
return False
49+
50+
# Skip evaluation for landingpage version
51+
if current == LANDINGPAGE:
52+
return False
53+
54+
try:
55+
# We use the latest known version as reference instead of current date.
56+
# This is crucial because when update information refresh is disabled due to
57+
# unsupported Core version, using date would create a permanent unsupported state.
58+
# Even if the user updates to the last known version, the system would remain
59+
# unsupported in 4+ years. By using latest known version, updating Core to the
60+
# last known version makes the system supported again, allowing update refresh.
61+
#
62+
# Home Assistant uses CalVer versioning (2024.1, 2024.2, etc.) with monthly releases.
63+
# We consider versions more than 2 years behind as unsupported.
64+
if (
65+
latest.strategy != AwesomeVersionStrategy.CALVER
66+
or latest.year is None
67+
or latest.minor is None
68+
):
69+
return True # Invalid latest version format
70+
71+
# Calculate 24 months back from latest version
72+
cutoff_month = int(latest.minor)
73+
cutoff_year = int(latest.year) - 2
74+
75+
# Create cutoff version
76+
cutoff_version = AwesomeVersion(
77+
f"{cutoff_year}.{cutoff_month}",
78+
ensure_strategy=AwesomeVersionStrategy.CALVER,
79+
)
80+
81+
# Compare current version with the cutoff
82+
return current < cutoff_version
83+
84+
except (AwesomeVersionException, ValueError, IndexError) as err:
85+
# This is run regularly, avoid log spam by logging at debug level
86+
_LOGGER.debug(
87+
"Failed to parse Home Assistant version '%s' or latest version '%s': %s",
88+
current,
89+
latest,
90+
err,
91+
)
92+
# Consider non-parseable versions as unsupported
93+
return True

supervisor/resolution/evaluations/os_version.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
"""Evaluation class for OS version."""
22

3+
import logging
4+
35
from awesomeversion import AwesomeVersion, AwesomeVersionException
46

57
from ...const import CoreState
68
from ...coresys import CoreSys
79
from ..const import UnsupportedReason
810
from .base import EvaluateBase
911

12+
_LOGGER: logging.Logger = logging.getLogger(__name__)
13+
1014

1115
def setup(coresys: CoreSys) -> EvaluateBase:
1216
"""Initialize evaluation-setup function."""
@@ -47,5 +51,13 @@ async def evaluate(self) -> bool:
4751
last_supported_version = AwesomeVersion(f"{int(latest.major) - 4}.0")
4852
try:
4953
return current < last_supported_version
50-
except AwesomeVersionException:
54+
except AwesomeVersionException as err:
55+
# This is run regularly, avoid log spam by logging at debug level
56+
_LOGGER.debug(
57+
"Can't parse OS version '%s' or latest version '%s': %s",
58+
current,
59+
latest,
60+
err,
61+
)
62+
# Consider non-parseable versions as unsupported
5163
return True

supervisor/store/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@ async def load(self) -> None:
7474

7575
@Job(
7676
name="store_manager_reload",
77-
conditions=[JobCondition.SUPERVISOR_UPDATED, JobCondition.OS_SUPPORTED],
77+
conditions=[
78+
JobCondition.SUPERVISOR_UPDATED,
79+
JobCondition.OS_SUPPORTED,
80+
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
81+
],
7882
on_condition=StoreJobError,
7983
)
8084
async def reload(self, repository: Repository | None = None) -> None:
@@ -117,6 +121,7 @@ async def reload(self, repository: Repository | None = None) -> None:
117121
JobCondition.INTERNET_SYSTEM,
118122
JobCondition.SUPERVISOR_UPDATED,
119123
JobCondition.OS_SUPPORTED,
124+
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
120125
],
121126
on_condition=StoreJobError,
122127
)

supervisor/updater.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,11 @@ async def _check_connectivity(self, connectivity: bool):
247247

248248
@Job(
249249
name="updater_fetch_data",
250-
conditions=[JobCondition.INTERNET_SYSTEM, JobCondition.OS_SUPPORTED],
250+
conditions=[
251+
JobCondition.INTERNET_SYSTEM,
252+
JobCondition.OS_SUPPORTED,
253+
JobCondition.HOME_ASSISTANT_CORE_SUPPORTED,
254+
],
251255
on_condition=UpdaterJobError,
252256
throttle_period=timedelta(seconds=30),
253257
concurrency=JobConcurrency.QUEUE,

tests/jobs/test_job_decorator.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from supervisor.jobs.job_group import JobGroup
2626
from supervisor.os.manager import OSManager
2727
from supervisor.plugins.audio import PluginAudio
28-
from supervisor.resolution.const import UnhealthyReason
28+
from supervisor.resolution.const import UnhealthyReason, UnsupportedReason
2929
from supervisor.supervisor import Supervisor
3030
from supervisor.utils.dt import utcnow
3131

@@ -1384,3 +1384,34 @@ async def nested_method(self) -> None:
13841384

13851385
assert test.call_count == 2 # Should execute now
13861386
assert test.nested_call_count == 2 # Nested call should also execute
1387+
1388+
1389+
async def test_core_supported(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
1390+
"""Test the core_supported decorator."""
1391+
1392+
class TestClass:
1393+
"""Test class."""
1394+
1395+
def __init__(self, coresys: CoreSys):
1396+
"""Initialize the test class."""
1397+
self.coresys = coresys
1398+
1399+
@Job(
1400+
name="test_core_supported_execute",
1401+
conditions=[JobCondition.HOME_ASSISTANT_CORE_SUPPORTED],
1402+
)
1403+
async def execute(self):
1404+
"""Execute the class method."""
1405+
return True
1406+
1407+
test = TestClass(coresys)
1408+
assert await test.execute()
1409+
1410+
coresys.resolution.unsupported.append(UnsupportedReason.HOME_ASSISTANT_CORE_VERSION)
1411+
assert not await test.execute()
1412+
assert (
1413+
"blocked from execution, unsupported Home Assistant Core version" in caplog.text
1414+
)
1415+
1416+
coresys.jobs.ignore_conditions = [JobCondition.HOME_ASSISTANT_CORE_SUPPORTED]
1417+
assert await test.execute()

0 commit comments

Comments
 (0)