Extend resolution center (#2297)

* Extend resolution center

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* move forward

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* Rename it to fixups

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* Finish p1

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* Finish p1 - add files

Signed-off-by: Pascal Vizeli <pvizeli@syshack.ch>

* Finishup

* Add more tests

* Add test for suggestion

* Add more tests

* fix tests & isort

* address comments

* address comments v2

* fix isort

* Change reference handling
This commit is contained in:
Pascal Vizeli 2020-11-26 17:16:36 +01:00 committed by GitHub
parent 7cccbc682c
commit fda1b523ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 591 additions and 130 deletions

View File

@ -44,7 +44,7 @@ class APIResoulution(CoreSysAttributes):
suggestion = self.sys_resolution.get_suggestion(
request.match_info.get("suggestion")
)
await self.sys_resolution.dismiss_suggestion(suggestion)
self.sys_resolution.dismiss_suggestion(suggestion)
except ResolutionNotFound:
raise APIError("The supplied UUID is not a valid suggestion") from None
@ -53,6 +53,6 @@ class APIResoulution(CoreSysAttributes):
"""Dismiss issue."""
try:
issue = self.sys_resolution.get_issue(request.match_info.get("issue"))
await self.sys_resolution.dismiss_issue(issue)
self.sys_resolution.dismiss_issue(issue)
except ResolutionNotFound:
raise APIError("The supplied UUID is not a valid issue") from None

View File

@ -222,6 +222,7 @@ class Core(CoreSysAttributes):
# Upate Host/Deivce information
self.sys_create_task(self.sys_host.reload())
self.sys_create_task(self.sys_updater.reload())
self.sys_create_task(self.sys_resolution.fixup.run_autofix())
self.state = CoreState.RUNNING
_LOGGER.info("Supervisor is up and running")

View File

@ -295,6 +295,10 @@ class ResolutionNotFound(ResolutionError):
"""Raise if suggestion/issue was not found."""
class ResolutionFixupError(HassioError):
"""Rasie if a fixup fails."""
# Store

View File

@ -1,9 +1,11 @@
"""Supervisor resolution center."""
from datetime import time
import logging
from typing import List, Optional
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionError, ResolutionNotFound
from .check import ResolutionCheck
from .const import (
SCHEDULED_HEALTHCHECK,
ContextType,
@ -14,7 +16,7 @@ from .const import (
)
from .data import Issue, Suggestion
from .evaluate import ResolutionEvaluation
from .free_space import ResolutionStorage
from .fixup import ResolutionFixup
from .notify import ResolutionNotify
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -27,8 +29,9 @@ class ResolutionManager(CoreSysAttributes):
"""Initialize Resolution manager."""
self.coresys: CoreSys = coresys
self._evaluate = ResolutionEvaluation(coresys)
self._check = ResolutionCheck(coresys)
self._fixup = ResolutionFixup(coresys)
self._notify = ResolutionNotify(coresys)
self._storage = ResolutionStorage(coresys)
self._suggestions: List[Suggestion] = []
self._issues: List[Issue] = []
@ -41,9 +44,14 @@ class ResolutionManager(CoreSysAttributes):
return self._evaluate
@property
def storage(self) -> ResolutionStorage:
"""Return the ResolutionStorage class."""
return self._storage
def check(self) -> ResolutionCheck:
"""Return the ResolutionCheck class."""
return self._check
@property
def fixup(self) -> ResolutionFixup:
"""Return the ResolutionFixup class."""
return self._fixup
@property
def notify(self) -> ResolutionNotify:
@ -133,11 +141,11 @@ class ResolutionManager(CoreSysAttributes):
# Schedule the healthcheck
self.sys_scheduler.register_task(self.healthcheck, SCHEDULED_HEALTHCHECK)
self.sys_scheduler.register_task(self.fixup.run_autofix, time(hour=2))
async def healthcheck(self):
"""Scheduled task to check for known issues."""
# Check free space
self.sys_run_in_executor(self.storage.check_free_space)
await self.check.check_system()
# Create notification for any known issues
await self.notify.issue_notifications()
@ -148,30 +156,24 @@ class ResolutionManager(CoreSysAttributes):
_LOGGER.warning("Suggestion %s is not valid", suggestion.uuid)
raise ResolutionError()
if suggestion.type == SuggestionType.CLEAR_FULL_SNAPSHOT:
self.storage.clean_full_snapshots()
elif suggestion.type == SuggestionType.CREATE_FULL_SNAPSHOT:
await self.sys_snapshots.do_snapshot_full()
self._suggestions.remove(suggestion)
await self.fixup.apply_fixup(suggestion)
await self.healthcheck()
async def dismiss_suggestion(self, suggestion: Suggestion) -> None:
def dismiss_suggestion(self, suggestion: Suggestion) -> None:
"""Dismiss suggested action."""
if suggestion not in self._suggestions:
_LOGGER.warning("The UUID %s is not valid suggestion", suggestion.uuid)
raise ResolutionError()
self._suggestions.remove(suggestion)
async def dismiss_issue(self, issue: Issue) -> None:
def dismiss_issue(self, issue: Issue) -> None:
"""Dismiss suggested action."""
if issue not in self._issues:
_LOGGER.warning("The UUID %s is not a valid issue", issue.uuid)
raise ResolutionError()
self._issues.remove(issue)
async def dismiss_unsupported(self, reason: Issue) -> None:
def dismiss_unsupported(self, reason: Issue) -> None:
"""Dismiss a reason for unsupported."""
if reason not in self._unsupported:
_LOGGER.warning("The reason %s is not active", reason)

View File

@ -0,0 +1,38 @@
"""Helpers to checks the system."""
import logging
from typing import List
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError
from .checks.base import CheckBase
from .checks.free_space import CheckFreeSpace
_LOGGER: logging.Logger = logging.getLogger(__name__)
class ResolutionCheck(CoreSysAttributes):
"""Checks class for resolution."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the checks class."""
self.coresys = coresys
self._free_space = CheckFreeSpace(coresys)
@property
def all_tests(self) -> List[CheckBase]:
"""Return all list of all checks."""
return [self._free_space]
async def check_system(self) -> None:
"""Check the system."""
_LOGGER.info("Starting system checks with state %s", self.sys_core.state)
for test in self.all_tests:
try:
await test()
except HassioError as err:
_LOGGER.warning("Error during processing %s: %s", test.issue, err)
self.sys_capture_exception(err)
_LOGGER.info("System checks complete")

View File

@ -0,0 +1 @@
"""Initialize system."""

View File

@ -0,0 +1,51 @@
"""Baseclass for system checks."""
from abc import ABC, abstractmethod, abstractproperty
import logging
from typing import List
from ...const import CoreState
from ...coresys import CoreSys, CoreSysAttributes
from ..const import ContextType, IssueType
_LOGGER: logging.Logger = logging.getLogger(__name__)
class CheckBase(ABC, CoreSysAttributes):
"""Baseclass for check."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the checks class."""
self.coresys = coresys
async def __call__(self) -> None:
"""Execute the evaluation."""
if self.sys_core.state not in self.states:
return
# Don't need run if issue exists
for issue in self.sys_resolution.issues:
if issue.type != self.issue or issue.context != self.context:
continue
return
_LOGGER.debug("Run check for %s/%s", self.issue, self.context)
await self.run_check()
@abstractmethod
async def run_check(self):
"""Run check."""
@property
@abstractproperty
def issue(self) -> IssueType:
"""Return a IssueType enum."""
@property
@abstractproperty
def context(self) -> ContextType:
"""Return a ContextType enum."""
@property
def states(self) -> List[CoreState]:
"""Return a list of valid states when this check can run."""
return []

View File

@ -2,29 +2,25 @@
import logging
from typing import List
from ..const import SNAPSHOT_FULL
from ..coresys import CoreSys, CoreSysAttributes
from .const import (
from ...const import SNAPSHOT_FULL, CoreState
from ..const import (
MINIMUM_FREE_SPACE_THRESHOLD,
MINIMUM_FULL_SNAPSHOTS,
ContextType,
IssueType,
SuggestionType,
)
from .data import Suggestion
from ..data import Suggestion
from .base import CheckBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
class ResolutionStorage(CoreSysAttributes):
"""Storage class for resolution."""
class CheckFreeSpace(CheckBase):
"""Storage class for check."""
def __init__(self, coresys: CoreSys):
"""Initialize the storage class."""
self.coresys = coresys
def check_free_space(self) -> None:
"""Check free space."""
async def run_check(self):
"""Run check."""
if self.sys_host.info.free_space > MINIMUM_FREE_SPACE_THRESHOLD:
if len(self.sys_snapshots.list_snapshots) == 0:
# No snapshots, let's suggest the user to create one!
@ -50,15 +46,17 @@ class ResolutionStorage(CoreSysAttributes):
IssueType.FREE_SPACE, ContextType.SYSTEM, suggestions=suggestions
)
def clean_full_snapshots(self):
"""Clean out all old full snapshots, but keep the most recent."""
full_snapshots = [
x for x in self.sys_snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL
]
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.FREE_SPACE
if len(full_snapshots) < MINIMUM_FULL_SNAPSHOTS:
return
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM
_LOGGER.info("Starting removal of old full snapshots")
for snapshot in sorted(full_snapshots, key=lambda x: x.date)[:-1]:
self.sys_snapshots.remove(snapshot)
@property
def states(self) -> List[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.RUNNING, CoreState.STARTUP]

View File

@ -1,8 +1,12 @@
"""Helpers to evaluate the system."""
import logging
from typing import List
from supervisor.exceptions import HassioError
from ..coresys import CoreSys, CoreSysAttributes
from .const import UnhealthyReason, UnsupportedReason
from .evaluations.base import EvaluateBase
from .evaluations.container import EvaluateContainer
from .evaluations.dbus import EvaluateDbus
from .evaluations.docker_configuration import EvaluateDockerConfiguration
@ -42,19 +46,34 @@ class ResolutionEvaluation(CoreSysAttributes):
self._systemd = EvaluateSystemd(coresys)
self._job_conditions = EvaluateJobConditions(coresys)
@property
def all_evalutions(self) -> List[EvaluateBase]:
"""Return list of all evaluations."""
return [
self._container,
self._dbus,
self._docker_configuration,
self._docker_version,
self._lxc,
self._network_manager,
self._operating_system,
self._privileged,
self._systemd,
self._job_conditions,
]
async def evaluate_system(self) -> None:
"""Evaluate the system."""
_LOGGER.info("Starting system evaluation with state %s", self.sys_core.state)
await self._container()
await self._dbus()
await self._docker_configuration()
await self._docker_version()
await self._lxc()
await self._network_manager()
await self._operating_system()
await self._privileged()
await self._systemd()
await self._job_conditions()
for evaluation in self.all_evalutions:
try:
await evaluation()
except HassioError as err:
_LOGGER.warning(
"Error during processing %s: %s", evaluation.reason, err
)
self.sys_capture_exception(err)
if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY):
self.sys_resolution.unhealthy = UnhealthyReason.DOCKER

View File

@ -1,4 +1,5 @@
"""Baseclass for system evaluations."""
from abc import ABC, abstractmethod, abstractproperty
import logging
from typing import List
@ -9,7 +10,7 @@ from ..const import UnsupportedReason
_LOGGER: logging.Logger = logging.getLogger(__name__)
class EvaluateBase(CoreSysAttributes):
class EvaluateBase(ABC, CoreSysAttributes):
"""Baseclass for evaluation."""
def __init__(self, coresys: CoreSys) -> None:
@ -31,21 +32,21 @@ class EvaluateBase(CoreSysAttributes):
else:
if self.reason in self.sys_resolution.unsupported:
_LOGGER.info("Clearing %s as reason for unsupported", self.reason)
await self.sys_resolution.dismiss_unsupported(self.reason)
self.sys_resolution.dismiss_unsupported(self.reason)
@abstractmethod
async def evaluate(self):
"""Run evaluation."""
raise NotImplementedError
@property
@abstractproperty
def reason(self) -> UnsupportedReason:
"""Return a UnsupportedReason enum."""
raise NotImplementedError
@property
@abstractproperty
def on_failure(self) -> str:
"""Return a string that is printed when self.evaluate is False."""
raise NotImplementedError
@property
def states(self) -> List[CoreState]:

View File

@ -0,0 +1,51 @@
"""Helpers to fixup the system."""
import logging
from typing import List
from supervisor.exceptions import HassioError
from supervisor.resolution.data import Suggestion
from ..coresys import CoreSys, CoreSysAttributes
from .fixups.base import FixupBase
from .fixups.clear_full_snapshot import FixupClearFullSnapshot
from .fixups.create_full_snapshot import FixupCreateFullSnapshot
_LOGGER: logging.Logger = logging.getLogger(__name__)
class ResolutionFixup(CoreSysAttributes):
"""Suggestion class for resolution."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the suggestion class."""
self.coresys = coresys
self._create_full_snapshot = FixupCreateFullSnapshot(coresys)
self._clear_full_snapshot = FixupClearFullSnapshot(coresys)
@property
def all_fixes(self) -> List[FixupBase]:
"""Return a list of all fixups."""
return [self._create_full_snapshot, self._clear_full_snapshot]
async def run_autofix(self) -> None:
"""Run all startup fixes."""
_LOGGER.info("Starting system autofix at state %s", self.sys_core.state)
for fix in self.all_fixes:
if not fix.auto:
continue
try:
await fix()
except HassioError as err:
_LOGGER.warning("Error during processing %s: %s", fix.suggestion, err)
self.sys_capture_exception(err)
_LOGGER.info("System autofix complete")
async def apply_fixup(self, suggestion: Suggestion) -> None:
"""Apply a fixup for a suggestion."""
for fix in self.all_fixes:
if fix.suggestion != suggestion.type or fix.context != suggestion.context:
continue
await fix()

View File

@ -0,0 +1 @@
"""Initialize system."""

View File

@ -0,0 +1,75 @@
"""Baseclass for system fixup."""
from abc import ABC, abstractmethod, abstractproperty
from contextlib import suppress
import logging
from typing import Optional
from ...coresys import CoreSys, CoreSysAttributes
from ...exceptions import ResolutionError, ResolutionFixupError
from ..const import ContextType, IssueType, SuggestionType
from ..data import Issue, Suggestion
_LOGGER: logging.Logger = logging.getLogger(__name__)
class FixupBase(ABC, CoreSysAttributes):
"""Baseclass for fixup."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the fixup class."""
self.coresys = coresys
async def __call__(self) -> None:
"""Execute the evaluation."""
# Get suggestion to fix
fixing_suggestion: Optional[Suggestion] = None
for suggestion in self.sys_resolution.suggestions:
if suggestion.type != self.suggestion or suggestion.context != self.context:
continue
fixing_suggestion = suggestion
break
# No suggestion
if fixing_suggestion is None:
return
# Process fixup
_LOGGER.debug("Run fixup for %s/%s", self.suggestion, self.context)
try:
await self.process_fixup(reference=fixing_suggestion.reference)
except ResolutionFixupError:
return
self.sys_resolution.dismiss_suggestion(fixing_suggestion)
if self.issue is None:
return
with suppress(ResolutionError):
self.sys_resolution.dismiss_issue(
Issue(self.issue, self.context, fixing_suggestion.reference)
)
@abstractmethod
async def process_fixup(self, reference: Optional[str] = None) -> None:
"""Run processing of fixup."""
@property
@abstractproperty
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
@property
@abstractproperty
def context(self) -> ContextType:
"""Return a ContextType enum."""
@property
def issue(self) -> Optional[IssueType]:
"""Return a IssueType enum."""
return None
@property
def auto(self) -> bool:
"""Return if a fixup can be apply as auto fix."""
return False

View File

@ -0,0 +1,41 @@
"""Helpers to check and fix issues with free space."""
import logging
from typing import Optional
from ...const import SNAPSHOT_FULL
from ..const import MINIMUM_FULL_SNAPSHOTS, ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
class FixupClearFullSnapshot(FixupBase):
"""Storage class for fixup."""
async def process_fixup(self, reference: Optional[str] = None) -> None:
"""Initialize the fixup class."""
full_snapshots = [
x for x in self.sys_snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL
]
if len(full_snapshots) < MINIMUM_FULL_SNAPSHOTS:
return
_LOGGER.info("Starting removal of old full snapshots")
for snapshot in sorted(full_snapshots, key=lambda x: x.date)[:-1]:
self.sys_snapshots.remove(snapshot)
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
return SuggestionType.CLEAR_FULL_SNAPSHOT
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.FREE_SPACE

View File

@ -0,0 +1,27 @@
"""Helpers to check and fix issues with free space."""
import logging
from typing import Optional
from ..const import ContextType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
class FixupCreateFullSnapshot(FixupBase):
"""Storage class for fixup."""
async def process_fixup(self, reference: Optional[str] = None) -> None:
"""Initialize the fixup class."""
_LOGGER.info("Create a full snapshot as backup")
await self.sys_snapshots.do_snapshot_full()
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
return SuggestionType.CREATE_FULL_SNAPSHOT
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM

View File

@ -11,6 +11,7 @@ import git
from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import StoreGitError
from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..validate import RE_REPOSITORY
from .utils import get_hash_from_repository
@ -81,6 +82,7 @@ class GitRepo(CoreSysAttributes):
)
raise StoreGitError() from err
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM])
async def clone(self) -> None:
"""Clone git add-on repository."""
async with self.lock:
@ -117,6 +119,7 @@ class GitRepo(CoreSysAttributes):
)
raise StoreGitError() from err
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM])
async def pull(self):
"""Pull Git add-on repo."""
if self.lock.locked():

View File

@ -0,0 +1,39 @@
"""Test check."""
# pylint: disable=import-error
from unittest.mock import patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.const import IssueType
async def test_check_setup(coresys: CoreSys):
"""Test check for setup."""
coresys.core.state = CoreState.SETUP
with patch(
"supervisor.resolution.checks.free_space.CheckFreeSpace.run_check",
return_value=False,
) as free_space:
await coresys.resolution.check.check_system()
free_space.assert_not_called()
async def test_check_running(coresys: CoreSys):
"""Test check for setup."""
coresys.core.state = CoreState.RUNNING
with patch(
"supervisor.resolution.checks.free_space.CheckFreeSpace.run_check",
return_value=False,
) as free_space:
await coresys.resolution.check.check_system()
free_space.assert_called_once()
async def test_if_check_make_issue(coresys: CoreSys):
"""Test check for setup."""
coresys.core.state = CoreState.RUNNING
with patch("shutil.disk_usage", return_value=(1, 1, 1)):
await coresys.resolution.check.check_system()
assert coresys.resolution.issues[-1].type == IssueType.FREE_SPACE

View File

@ -0,0 +1,51 @@
"""Test evaluation base."""
# pylint: disable=import-error,protected-access
from unittest.mock import patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.free_space import CheckFreeSpace
from supervisor.resolution.const import IssueType
async def test_check(coresys: CoreSys):
"""Test check."""
free_space = CheckFreeSpace(coresys)
coresys.core.state = CoreState.RUNNING
assert len(coresys.resolution.issues) == 0
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0 ** 3))):
await free_space.run_check()
assert len(coresys.resolution.issues) == 0
with patch("shutil.disk_usage", return_value=(1, 1, 1)):
await free_space.run_check()
assert coresys.resolution.issues[-1].type == IssueType.FREE_SPACE
async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
free_space = CheckFreeSpace(coresys)
should_run = free_space.states
should_not_run = [state for state in CoreState if state not in should_run]
assert len(should_run) != 0
assert len(should_not_run) != 0
with patch(
"supervisor.resolution.checks.free_space.CheckFreeSpace.run_check",
return_value=None,
) as check:
for state in should_run:
coresys.core.state = state
await free_space()
check.assert_called_once()
check.reset_mock()
for state in should_not_run:
coresys.core.state = state
await free_space()
check.assert_not_called()
check.reset_mock()

View File

@ -1,21 +0,0 @@
"""Test evaluation base."""
# pylint: disable=import-error
import pytest
from supervisor.coresys import CoreSys
from supervisor.resolution.evaluations.base import EvaluateBase
async def test_evaluation_base(coresys: CoreSys):
"""Test evaluation base."""
base = EvaluateBase(coresys)
assert not base.states
with pytest.raises(NotImplementedError):
await base.evaluate()
with pytest.raises(NotImplementedError):
assert not base.on_failure
with pytest.raises(NotImplementedError):
assert not base.reason

View File

@ -0,0 +1,64 @@
"""Test evaluation base."""
# pylint: disable=import-error,protected-access
from pathlib import Path
from supervisor.const import (
ATTR_DATE,
ATTR_SLUG,
ATTR_TYPE,
SNAPSHOT_FULL,
SNAPSHOT_PARTIAL,
)
from supervisor.coresys import CoreSys
from supervisor.resolution.const import ContextType, SuggestionType
from supervisor.resolution.data import Suggestion
from supervisor.resolution.fixups.clear_full_snapshot import FixupClearFullSnapshot
from supervisor.snapshots.snapshot import Snapshot
from supervisor.utils.dt import utcnow
from supervisor.utils.tar import SecureTarFile
async def test_fixup(coresys: CoreSys, tmp_path):
"""Test fixup."""
clear_full_snapshot = FixupClearFullSnapshot(coresys)
assert not clear_full_snapshot.auto
coresys.resolution.suggestions = Suggestion(
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
)
for slug in ["sn1", "sn2", "sn3", "sn4", "sn5"]:
temp_tar = Path(tmp_path, f"{slug}.tar")
with SecureTarFile(temp_tar, "w"):
pass
snapshot = Snapshot(coresys, temp_tar)
snapshot._data = { # pylint: disable=protected-access
ATTR_SLUG: slug,
ATTR_DATE: utcnow().isoformat(),
ATTR_TYPE: SNAPSHOT_PARTIAL
if "1" in slug or "5" in slug
else SNAPSHOT_FULL,
}
coresys.snapshots.snapshots_obj[snapshot.slug] = snapshot
newest_full_snapshot = coresys.snapshots.snapshots_obj["sn4"]
assert newest_full_snapshot in coresys.snapshots.list_snapshots
assert (
len(
[x for x in coresys.snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL]
)
== 3
)
await clear_full_snapshot()
assert newest_full_snapshot in coresys.snapshots.list_snapshots
assert (
len(
[x for x in coresys.snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL]
)
== 1
)
assert len(coresys.resolution.suggestions) == 0

View File

@ -0,0 +1,27 @@
"""Test evaluation base."""
# pylint: disable=import-error,protected-access
from unittest.mock import AsyncMock
from supervisor.coresys import CoreSys
from supervisor.resolution.const import ContextType, SuggestionType
from supervisor.resolution.data import Suggestion
from supervisor.resolution.fixups.create_full_snapshot import FixupCreateFullSnapshot
async def test_fixup(coresys: CoreSys):
"""Test fixup."""
create_full_snapshot = FixupCreateFullSnapshot(coresys)
assert not create_full_snapshot.auto
coresys.resolution.suggestions = Suggestion(
SuggestionType.CREATE_FULL_SNAPSHOT, ContextType.SYSTEM
)
mock_snapshots = AsyncMock()
coresys.snapshots.do_snapshot_full = mock_snapshots
await create_full_snapshot()
mock_snapshots.assert_called()
assert len(coresys.resolution.suggestions) == 0

View File

@ -0,0 +1,35 @@
"""Test check."""
# pylint: disable=import-error, protected-access
from unittest.mock import AsyncMock, patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.const import ContextType, SuggestionType
from supervisor.resolution.data import Suggestion
async def test_check_autofix(coresys: CoreSys):
"""Test check for setup."""
coresys.core.state = CoreState.RUNNING
coresys.resolution.fixup._create_full_snapshot.process_fixup = AsyncMock()
with patch(
"supervisor.resolution.fixups.create_full_snapshot.FixupCreateFullSnapshot.auto",
return_value=True,
):
await coresys.resolution.fixup.run_autofix()
coresys.resolution.fixup._create_full_snapshot.process_fixup.assert_not_called()
coresys.resolution.suggestions = Suggestion(
SuggestionType.CREATE_FULL_SNAPSHOT, ContextType.SYSTEM
)
with patch(
"supervisor.resolution.fixups.create_full_snapshot.FixupCreateFullSnapshot.auto",
return_value=True,
):
await coresys.resolution.fixup.run_autofix()
coresys.resolution.fixup._create_full_snapshot.process_fixup.assert_called_once()
assert len(coresys.resolution.suggestions) == 0

View File

@ -1,16 +1,8 @@
"""Tests for resolution manager."""
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
from supervisor.const import (
ATTR_DATE,
ATTR_SLUG,
ATTR_TYPE,
SNAPSHOT_FULL,
SNAPSHOT_PARTIAL,
)
from supervisor.coresys import CoreSys
from supervisor.exceptions import ResolutionError
from supervisor.resolution.const import (
@ -21,9 +13,6 @@ from supervisor.resolution.const import (
UnsupportedReason,
)
from supervisor.resolution.data import Issue, Suggestion
from supervisor.snapshots.snapshot import Snapshot
from supervisor.utils.dt import utcnow
from supervisor.utils.tar import SecureTarFile
def test_properies_unsupported(coresys: CoreSys):
@ -44,42 +33,6 @@ def test_properies_unhealthy(coresys: CoreSys):
assert not coresys.core.healthy
async def test_clear_snapshots(coresys: CoreSys, tmp_path):
"""Test snapshot cleanup."""
for slug in ["sn1", "sn2", "sn3", "sn4", "sn5"]:
temp_tar = Path(tmp_path, f"{slug}.tar")
with SecureTarFile(temp_tar, "w"):
pass
snapshot = Snapshot(coresys, temp_tar)
snapshot._data = { # pylint: disable=protected-access
ATTR_SLUG: slug,
ATTR_DATE: utcnow().isoformat(),
ATTR_TYPE: SNAPSHOT_PARTIAL
if "1" in slug or "5" in slug
else SNAPSHOT_FULL,
}
coresys.snapshots.snapshots_obj[snapshot.slug] = snapshot
newest_full_snapshot = coresys.snapshots.snapshots_obj["sn4"]
assert newest_full_snapshot in coresys.snapshots.list_snapshots
assert (
len(
[x for x in coresys.snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL]
)
== 3
)
coresys.resolution.storage.clean_full_snapshots()
assert newest_full_snapshot in coresys.snapshots.list_snapshots
assert (
len(
[x for x in coresys.snapshots.list_snapshots if x.sys_type == SNAPSHOT_FULL]
)
== 1
)
@pytest.mark.asyncio
async def test_resolution_dismiss_suggestion(coresys: CoreSys):
"""Test resolution manager suggestion apply api."""
@ -88,11 +41,11 @@ async def test_resolution_dismiss_suggestion(coresys: CoreSys):
)
assert SuggestionType.CLEAR_FULL_SNAPSHOT == coresys.resolution.suggestions[-1].type
await coresys.resolution.dismiss_suggestion(clear_snapshot)
coresys.resolution.dismiss_suggestion(clear_snapshot)
assert clear_snapshot not in coresys.resolution.suggestions
with pytest.raises(ResolutionError):
await coresys.resolution.dismiss_suggestion(clear_snapshot)
coresys.resolution.dismiss_suggestion(clear_snapshot)
@pytest.mark.asyncio
@ -131,11 +84,11 @@ async def test_resolution_dismiss_issue(coresys: CoreSys):
)
assert IssueType.UPDATE_FAILED == coresys.resolution.issues[-1].type
await coresys.resolution.dismiss_issue(updated_failed)
coresys.resolution.dismiss_issue(updated_failed)
assert updated_failed not in coresys.resolution.issues
with pytest.raises(ResolutionError):
await coresys.resolution.dismiss_issue(updated_failed)
coresys.resolution.dismiss_issue(updated_failed)
@pytest.mark.asyncio
@ -161,8 +114,8 @@ async def test_resolution_dismiss_unsupported(coresys: CoreSys):
"""Test resolution manager dismiss unsupported reason."""
coresys.resolution.unsupported = UnsupportedReason.CONTAINER
await coresys.resolution.dismiss_unsupported(UnsupportedReason.CONTAINER)
coresys.resolution.dismiss_unsupported(UnsupportedReason.CONTAINER)
assert UnsupportedReason.CONTAINER not in coresys.resolution.unsupported
with pytest.raises(ResolutionError):
await coresys.resolution.dismiss_unsupported(UnsupportedReason.CONTAINER)
coresys.resolution.dismiss_unsupported(UnsupportedReason.CONTAINER)