Persistent notifications to repairs and fix free_space check (#6179)

* Persistent notifications to repairs and fix free_space check

* Fix tests mocking too little free space
This commit is contained in:
Mike Degatano
2025-09-16 11:22:59 -04:00
committed by GitHub
parent 857dae7736
commit 01911a44cd
7 changed files with 30 additions and 106 deletions

View File

@@ -1,15 +1,8 @@
"""Helpers to check and fix issues with free space."""
from ...backups.const import BackupType
from ...const import CoreState
from ...coresys import CoreSys
from ..const import (
MINIMUM_FREE_SPACE_THRESHOLD,
MINIMUM_FULL_BACKUPS,
ContextType,
IssueType,
SuggestionType,
)
from ..const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
from .base import CheckBase
@@ -23,31 +16,12 @@ class CheckFreeSpace(CheckBase):
async def run_check(self) -> None:
"""Run check if not affected by issue."""
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
return
suggestions: list[SuggestionType] = []
if (
len(
[
backup
for backup in self.sys_backups.list_backups
if backup.sys_type == BackupType.FULL
]
)
> MINIMUM_FULL_BACKUPS
):
suggestions.append(SuggestionType.CLEAR_FULL_BACKUP)
self.sys_resolution.create_issue(
IssueType.FREE_SPACE, ContextType.SYSTEM, suggestions=suggestions
)
if await self.approve_check():
self.sys_resolution.create_issue(IssueType.FREE_SPACE, ContextType.SYSTEM)
async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
return False
return True
return await self.sys_host.info.free_space() <= MINIMUM_FREE_SPACE_THRESHOLD
@property
def issue(self) -> IssueType:

View File

@@ -9,7 +9,7 @@ FILE_CONFIG_RESOLUTION = Path(SUPERVISOR_DATA, "resolution.json")
SCHEDULED_HEALTHCHECK = 3600
MINIMUM_FREE_SPACE_THRESHOLD = 1
MINIMUM_FREE_SPACE_THRESHOLD = 2
MINIMUM_FULL_BACKUPS = 2
DNS_CHECK_HOST = "_checkdns.home-assistant.io"

View File

@@ -10,9 +10,16 @@ from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HomeAssistantAPIError
from .checks.core_security import SecurityReference
from .const import ContextType, IssueType
from .data import Issue
_LOGGER: logging.Logger = logging.getLogger(__name__)
ISSUE_SECURITY_CUSTOM_COMP_2021_1_5 = Issue(
IssueType.SECURITY,
ContextType.CORE,
reference=SecurityReference.CUSTOM_COMPONENTS_BELOW_2021_1_5,
)
class ResolutionNotify(CoreSysAttributes):
"""Notify class for resolution."""
@@ -29,44 +36,17 @@ class ResolutionNotify(CoreSysAttributes):
):
return
messages = []
for issue in self.sys_resolution.issues:
if issue.type == IssueType.FREE_SPACE:
messages.append(
{
"title": "Available space is less than 1GB!",
"message": f"Available space is {await self.sys_host.info.free_space()}GB, see https://www.home-assistant.io/more-info/free-space for more information.",
"notification_id": "supervisor_issue_free_space",
}
)
if issue.type == IssueType.SECURITY and issue.context == ContextType.CORE:
if (
issue.reference
== SecurityReference.CUSTOM_COMPONENTS_BELOW_2021_1_5
):
messages.append(
{
"title": "Security notification",
"message": "The Supervisor detected that this version of Home Assistant could be insecure in combination with custom integrations. [Update as soon as possible.](/hassio/dashboard)\n\nFor more information see the [Security alert](https://www.home-assistant.io/latest-security-alert).",
"notification_id": "supervisor_update_home_assistant_2021_1_5",
}
)
if issue.type == IssueType.PWNED and issue.context == ContextType.ADDON:
messages.append(
{
"title": f"Insecure secrets in {issue.reference}",
"message": f"The add-on {issue.reference} uses secrets which are detected as not secure, see https://www.home-assistant.io/more-info/pwned-passwords for more information.",
"notification_id": f"supervisor_issue_pwned_{issue.reference}",
}
)
for message in messages:
# This one issue must remain a persistent notification rather then a repair because repairs didn't exist in HA 2021.1.5
if ISSUE_SECURITY_CUSTOM_COMP_2021_1_5 in self.sys_resolution.issues:
try:
async with self.sys_homeassistant.api.make_request(
"post",
"api/services/persistent_notification/create",
json=message,
json={
"title": "Security notification",
"message": "The Supervisor detected that this version of Home Assistant could be insecure in combination with custom integrations. [Update as soon as possible.](/hassio/dashboard)\n\nFor more information see the [Security alert](https://www.home-assistant.io/latest-security-alert).",
"notification_id": "supervisor_update_home_assistant_2021_1_5",
},
) as resp:
if resp.status in (200, 201):
_LOGGER.debug("Successfully created persistent_notification")

View File

@@ -139,10 +139,10 @@ async def test_free_space(coresys: CoreSys):
return True
test = TestClass(coresys)
with patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))):
with patch("shutil.disk_usage", return_value=(42, 42, (2048.0**3))):
assert await test.execute()
with patch("shutil.disk_usage", return_value=(42, 42, (512.0**3))):
with patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))):
assert not await test.execute()
coresys.jobs.ignore_conditions = [JobCondition.FREE_SPACE]

View File

@@ -70,7 +70,7 @@ async def test_if_check_cleanup_issue(coresys: CoreSys):
assert free_space in coresys.resolution.issues
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
with patch("shutil.disk_usage", return_value=(42, 42, 3 * (1024.0**3))):
await coresys.resolution.check.check_system()
assert free_space not in coresys.resolution.issues

View File

@@ -1,33 +1,12 @@
"""Test check free space fixup."""
# pylint: disable=import-error,protected-access
from unittest.mock import MagicMock, PropertyMock, patch
from unittest.mock import patch
import pytest
from supervisor.backups.const import BackupType
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.free_space import CheckFreeSpace
from supervisor.resolution.const import IssueType, SuggestionType
@pytest.fixture(name="suggestion")
async def fixture_suggestion(
coresys: CoreSys, request: pytest.FixtureRequest
) -> SuggestionType | None:
"""Set up test for suggestion."""
if request.param == SuggestionType.CLEAR_FULL_BACKUP:
backup = MagicMock()
backup.sys_type = BackupType.FULL
with patch.object(
type(coresys.backups),
"list_backups",
new=PropertyMock(return_value=[backup, backup, backup]),
):
yield SuggestionType.CLEAR_FULL_BACKUP
else:
yield request.param
from supervisor.resolution.const import IssueType
async def test_base(coresys: CoreSys):
@@ -37,19 +16,14 @@ async def test_base(coresys: CoreSys):
assert free_space.enabled
@pytest.mark.parametrize(
"suggestion",
[None, SuggestionType.CLEAR_FULL_BACKUP],
indirect=True,
)
async def test_check(coresys: CoreSys, suggestion: SuggestionType | None):
async def test_check(coresys: CoreSys):
"""Test check."""
free_space = CheckFreeSpace(coresys)
await coresys.core.set_state(CoreState.RUNNING)
assert len(coresys.resolution.issues) == 0
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
with patch("shutil.disk_usage", return_value=(42, 42, 3 * (1024.0**3))):
await free_space.run_check()
assert len(coresys.resolution.issues) == 0
@@ -58,10 +32,6 @@ async def test_check(coresys: CoreSys, suggestion: SuggestionType | None):
await free_space.run_check()
assert coresys.resolution.issues[-1].type == IssueType.FREE_SPACE
if suggestion:
assert coresys.resolution.suggestions[-1].type == suggestion
else:
assert len(coresys.resolution.suggestions) == 0
@@ -73,7 +43,7 @@ async def test_approve(coresys: CoreSys):
with patch("shutil.disk_usage", return_value=(1, 1, 1)):
assert await free_space.approve_check()
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
with patch("shutil.disk_usage", return_value=(42, 42, 3 * (1024.0**3))):
assert not await free_space.approve_check()

View File

@@ -170,7 +170,7 @@ async def test_update_unavailable_addon(
"version",
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
),
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
patch("shutil.disk_usage", return_value=(42, 42, (5120.0**3))),
):
with pytest.raises(AddonNotSupportedError):
await coresys.addons.update("local_ssh", backup=True)
@@ -226,7 +226,7 @@ async def test_install_unavailable_addon(
"version",
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
),
patch("shutil.disk_usage", return_value=(42, 42, (1024.0**3))),
patch("shutil.disk_usage", return_value=(42, 42, (5120.0**3))),
pytest.raises(AddonNotSupportedError),
):
await coresys.addons.install("local_ssh")