mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-20 10:28:45 +00:00
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:
@@ -1,15 +1,8 @@
|
|||||||
"""Helpers to check and fix issues with free space."""
|
"""Helpers to check and fix issues with free space."""
|
||||||
|
|
||||||
from ...backups.const import BackupType
|
|
||||||
from ...const import CoreState
|
from ...const import CoreState
|
||||||
from ...coresys import CoreSys
|
from ...coresys import CoreSys
|
||||||
from ..const import (
|
from ..const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
|
||||||
MINIMUM_FREE_SPACE_THRESHOLD,
|
|
||||||
MINIMUM_FULL_BACKUPS,
|
|
||||||
ContextType,
|
|
||||||
IssueType,
|
|
||||||
SuggestionType,
|
|
||||||
)
|
|
||||||
from .base import CheckBase
|
from .base import CheckBase
|
||||||
|
|
||||||
|
|
||||||
@@ -23,31 +16,12 @@ class CheckFreeSpace(CheckBase):
|
|||||||
|
|
||||||
async def run_check(self) -> None:
|
async def run_check(self) -> None:
|
||||||
"""Run check if not affected by issue."""
|
"""Run check if not affected by issue."""
|
||||||
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
|
if await self.approve_check():
|
||||||
return
|
self.sys_resolution.create_issue(IssueType.FREE_SPACE, ContextType.SYSTEM)
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def approve_check(self, reference: str | None = None) -> bool:
|
async def approve_check(self, reference: str | None = None) -> bool:
|
||||||
"""Approve check if it is affected by issue."""
|
"""Approve check if it is affected by issue."""
|
||||||
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
|
return await self.sys_host.info.free_space() <= MINIMUM_FREE_SPACE_THRESHOLD
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def issue(self) -> IssueType:
|
def issue(self) -> IssueType:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ FILE_CONFIG_RESOLUTION = Path(SUPERVISOR_DATA, "resolution.json")
|
|||||||
|
|
||||||
SCHEDULED_HEALTHCHECK = 3600
|
SCHEDULED_HEALTHCHECK = 3600
|
||||||
|
|
||||||
MINIMUM_FREE_SPACE_THRESHOLD = 1
|
MINIMUM_FREE_SPACE_THRESHOLD = 2
|
||||||
MINIMUM_FULL_BACKUPS = 2
|
MINIMUM_FULL_BACKUPS = 2
|
||||||
|
|
||||||
DNS_CHECK_HOST = "_checkdns.home-assistant.io"
|
DNS_CHECK_HOST = "_checkdns.home-assistant.io"
|
||||||
|
|||||||
@@ -10,9 +10,16 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..exceptions import HomeAssistantAPIError
|
from ..exceptions import HomeAssistantAPIError
|
||||||
from .checks.core_security import SecurityReference
|
from .checks.core_security import SecurityReference
|
||||||
from .const import ContextType, IssueType
|
from .const import ContextType, IssueType
|
||||||
|
from .data import Issue
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_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):
|
class ResolutionNotify(CoreSysAttributes):
|
||||||
"""Notify class for resolution."""
|
"""Notify class for resolution."""
|
||||||
@@ -29,44 +36,17 @@ class ResolutionNotify(CoreSysAttributes):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
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:
|
||||||
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:
|
|
||||||
try:
|
try:
|
||||||
async with self.sys_homeassistant.api.make_request(
|
async with self.sys_homeassistant.api.make_request(
|
||||||
"post",
|
"post",
|
||||||
"api/services/persistent_notification/create",
|
"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:
|
) as resp:
|
||||||
if resp.status in (200, 201):
|
if resp.status in (200, 201):
|
||||||
_LOGGER.debug("Successfully created persistent_notification")
|
_LOGGER.debug("Successfully created persistent_notification")
|
||||||
|
|||||||
@@ -139,10 +139,10 @@ async def test_free_space(coresys: CoreSys):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
test = TestClass(coresys)
|
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()
|
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()
|
assert not await test.execute()
|
||||||
|
|
||||||
coresys.jobs.ignore_conditions = [JobCondition.FREE_SPACE]
|
coresys.jobs.ignore_conditions = [JobCondition.FREE_SPACE]
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async def test_if_check_cleanup_issue(coresys: CoreSys):
|
|||||||
|
|
||||||
assert free_space in coresys.resolution.issues
|
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()
|
await coresys.resolution.check.check_system()
|
||||||
|
|
||||||
assert free_space not in coresys.resolution.issues
|
assert free_space not in coresys.resolution.issues
|
||||||
|
|||||||
@@ -1,33 +1,12 @@
|
|||||||
"""Test check free space fixup."""
|
"""Test check free space fixup."""
|
||||||
|
|
||||||
# pylint: disable=import-error,protected-access
|
# 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.const import CoreState
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.resolution.checks.free_space import CheckFreeSpace
|
from supervisor.resolution.checks.free_space import CheckFreeSpace
|
||||||
from supervisor.resolution.const import IssueType, SuggestionType
|
from supervisor.resolution.const import IssueType
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
|
|
||||||
async def test_base(coresys: CoreSys):
|
async def test_base(coresys: CoreSys):
|
||||||
@@ -37,19 +16,14 @@ async def test_base(coresys: CoreSys):
|
|||||||
assert free_space.enabled
|
assert free_space.enabled
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
async def test_check(coresys: CoreSys):
|
||||||
"suggestion",
|
|
||||||
[None, SuggestionType.CLEAR_FULL_BACKUP],
|
|
||||||
indirect=True,
|
|
||||||
)
|
|
||||||
async def test_check(coresys: CoreSys, suggestion: SuggestionType | None):
|
|
||||||
"""Test check."""
|
"""Test check."""
|
||||||
free_space = CheckFreeSpace(coresys)
|
free_space = CheckFreeSpace(coresys)
|
||||||
await coresys.core.set_state(CoreState.RUNNING)
|
await coresys.core.set_state(CoreState.RUNNING)
|
||||||
|
|
||||||
assert len(coresys.resolution.issues) == 0
|
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()
|
await free_space.run_check()
|
||||||
|
|
||||||
assert len(coresys.resolution.issues) == 0
|
assert len(coresys.resolution.issues) == 0
|
||||||
@@ -58,11 +32,7 @@ async def test_check(coresys: CoreSys, suggestion: SuggestionType | None):
|
|||||||
await free_space.run_check()
|
await free_space.run_check()
|
||||||
|
|
||||||
assert coresys.resolution.issues[-1].type == IssueType.FREE_SPACE
|
assert coresys.resolution.issues[-1].type == IssueType.FREE_SPACE
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
if suggestion:
|
|
||||||
assert coresys.resolution.suggestions[-1].type == suggestion
|
|
||||||
else:
|
|
||||||
assert len(coresys.resolution.suggestions) == 0
|
|
||||||
|
|
||||||
|
|
||||||
async def test_approve(coresys: CoreSys):
|
async def test_approve(coresys: CoreSys):
|
||||||
@@ -73,7 +43,7 @@ async def test_approve(coresys: CoreSys):
|
|||||||
with patch("shutil.disk_usage", return_value=(1, 1, 1)):
|
with patch("shutil.disk_usage", return_value=(1, 1, 1)):
|
||||||
assert await free_space.approve_check()
|
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()
|
assert not await free_space.approve_check()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ async def test_update_unavailable_addon(
|
|||||||
"version",
|
"version",
|
||||||
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
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):
|
with pytest.raises(AddonNotSupportedError):
|
||||||
await coresys.addons.update("local_ssh", backup=True)
|
await coresys.addons.update("local_ssh", backup=True)
|
||||||
@@ -226,7 +226,7 @@ async def test_install_unavailable_addon(
|
|||||||
"version",
|
"version",
|
||||||
new=PropertyMock(return_value=AwesomeVersion("2022.1.1")),
|
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),
|
pytest.raises(AddonNotSupportedError),
|
||||||
):
|
):
|
||||||
await coresys.addons.install("local_ssh")
|
await coresys.addons.install("local_ssh")
|
||||||
|
|||||||
Reference in New Issue
Block a user