mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-10 02:36:29 +00:00
Resolution: extend type and context (#2130)
* Resolution: extend type and context * fix property * add helper * fix api * fix tests * Fix patch * finish tests * Update supervisor/resolution/const.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Update supervisor/resolution/const.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Fix type * fix lint * Update supervisor/api/resolution.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Update supervisor/resolution/__init__.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Update API & add more tests * Update supervisor/api/resolution.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Update supervisor/resolution/__init__.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * Update supervisor/resolution/__init__.py Co-authored-by: Joakim Sørensen <joasoe@gmail.com> * fix black * remove azure ci * fix test * fix tests * fix tests * fix tests p2 Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
parent
fe0e41adec
commit
d119e99001
@ -1,52 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- dev
|
||||
pr:
|
||||
- dev
|
||||
variables:
|
||||
- name: versionHadolint
|
||||
value: "v1.16.3"
|
||||
|
||||
jobs:
|
||||
- job: "Tox"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- script: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpulse0 libudev1
|
||||
displayName: "Install Host library"
|
||||
- task: UsePythonVersion@0
|
||||
displayName: "Use Python 3.8"
|
||||
inputs:
|
||||
versionSpec: "3.8"
|
||||
- script: pip install tox
|
||||
displayName: "Install Tox"
|
||||
- script: tox
|
||||
displayName: "Run Tox"
|
||||
- job: "JQ"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- script: sudo apt-get install -y jq
|
||||
displayName: "Install JQ"
|
||||
- bash: |
|
||||
shopt -s globstar
|
||||
cat **/*.json | jq '.'
|
||||
displayName: "Run JQ"
|
||||
- job: "Hadolint"
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- script: sudo docker pull hadolint/hadolint:$(versionHadolint)
|
||||
displayName: "Install Hadolint"
|
||||
- script: |
|
||||
sudo docker run --rm -i \
|
||||
-v $(pwd)/.hadolint.yaml:/.hadolint.yaml:ro \
|
||||
hadolint/hadolint:$(versionHadolint) < Dockerfile
|
||||
displayName: "Run Hadolint"
|
@ -200,11 +200,18 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/resolution", api_resolution.base),
|
||||
web.post("/resolution/{suggestion}", api_resolution.apply_suggestion),
|
||||
web.post(
|
||||
"/resolution/{suggestion}/dismiss",
|
||||
"/resolution/suggestion/{suggestion}",
|
||||
api_resolution.apply_suggestion,
|
||||
),
|
||||
web.delete(
|
||||
"/resolution/suggestion/{suggestion}",
|
||||
api_resolution.dismiss_suggestion,
|
||||
),
|
||||
web.delete(
|
||||
"/resolution/issue/{issue}",
|
||||
api_resolution.dismiss_issue,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiohttp import web
|
||||
import attr
|
||||
|
||||
from ..const import ATTR_ISSUES, ATTR_SUGGESTIONS, ATTR_UNSUPPORTED
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..resolution.const import Suggestion
|
||||
from ..exceptions import APIError, ResolutionNotFound
|
||||
from .utils import api_process
|
||||
|
||||
|
||||
@ -18,24 +18,40 @@ class APIResoulution(CoreSysAttributes):
|
||||
"""Return network information."""
|
||||
return {
|
||||
ATTR_UNSUPPORTED: self.sys_resolution.unsupported,
|
||||
ATTR_SUGGESTIONS: self.sys_resolution.suggestions,
|
||||
ATTR_ISSUES: self.sys_resolution.issues,
|
||||
ATTR_SUGGESTIONS: [
|
||||
attr.asdict(suggestion)
|
||||
for suggestion in self.sys_resolution.suggestions
|
||||
],
|
||||
ATTR_ISSUES: [attr.asdict(issue) for issue in self.sys_resolution.issues],
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def apply_suggestion(self, request: web.Request) -> None:
|
||||
"""Apply suggestion."""
|
||||
try:
|
||||
suggestion = Suggestion(request.match_info.get("suggestion"))
|
||||
suggestion = self.sys_resolution.get_suggestion(
|
||||
request.match_info.get("suggestion")
|
||||
)
|
||||
await self.sys_resolution.apply_suggestion(suggestion)
|
||||
except ValueError:
|
||||
raise APIError("Suggestion is not valid") from None
|
||||
except ResolutionNotFound:
|
||||
raise APIError("The supplied UUID is not a valid suggestion") from None
|
||||
|
||||
@api_process
|
||||
async def dismiss_suggestion(self, request: web.Request) -> None:
|
||||
"""Dismiss suggestion."""
|
||||
try:
|
||||
suggestion = Suggestion(request.match_info.get("suggestion"))
|
||||
suggestion = self.sys_resolution.get_suggestion(
|
||||
request.match_info.get("suggestion")
|
||||
)
|
||||
await self.sys_resolution.dismiss_suggestion(suggestion)
|
||||
except ValueError:
|
||||
raise APIError("Suggestion is not valid") from None
|
||||
except ResolutionNotFound:
|
||||
raise APIError("The supplied UUID is not a valid suggestion") from None
|
||||
|
||||
@api_process
|
||||
async def dismiss_issue(self, request: web.Request) -> None:
|
||||
"""Dismiss issue."""
|
||||
try:
|
||||
issue = self.sys_resolution.get_issue(request.match_info.get("issue"))
|
||||
await self.sys_resolution.dismiss_issue(issue)
|
||||
except ResolutionNotFound:
|
||||
raise APIError("The supplied UUID is not a valid issue") from None
|
||||
|
@ -22,7 +22,7 @@ from .exceptions import (
|
||||
HomeAssistantError,
|
||||
SupervisorUpdateError,
|
||||
)
|
||||
from .resolution.const import UnsupportedReason
|
||||
from .resolution.const import ContextType, IssueType, UnsupportedReason
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -107,6 +107,9 @@ class Core(CoreSysAttributes):
|
||||
self.sys_updater.channel,
|
||||
)
|
||||
elif self.sys_config.version != self.sys_supervisor.version:
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.UPDATE_ROLLBACK, ContextType.SUPERVISOR
|
||||
)
|
||||
self.healthy = False
|
||||
_LOGGER.error(
|
||||
"Update '%s' of Supervisor '%s' failed!",
|
||||
|
@ -270,3 +270,14 @@ class HardwareNotSupportedError(HassioNotSupportedError):
|
||||
|
||||
class PulseAudioError(HassioError):
|
||||
"""Raise if an sound error is happening."""
|
||||
|
||||
|
||||
# Resolution
|
||||
|
||||
|
||||
class ResolutionError(HassioError):
|
||||
"""Raise if an error is happning on resoltuion."""
|
||||
|
||||
|
||||
class ResolutionNotFound(ResolutionError):
|
||||
"""Raise if suggestion/issue was not found."""
|
||||
|
@ -21,6 +21,7 @@ from ..exceptions import (
|
||||
HomeAssistantError,
|
||||
HomeAssistantUpdateError,
|
||||
)
|
||||
from ..resolution.const import ContextType, IssueType
|
||||
from ..utils import convert_to_ascii, process_lock
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -191,6 +192,10 @@ class HomeAssistantCore(CoreSysAttributes):
|
||||
# Update going wrong, revert it
|
||||
if self.error_state and rollback:
|
||||
_LOGGER.critical("HomeAssistant update failed -> rollback!")
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.UPDATE_ROLLBACK, ContextType.CORE
|
||||
)
|
||||
|
||||
# Make a copy of the current log file if it exsist
|
||||
logfile = self.sys_config.path_homeassistant / "home-assistant.log"
|
||||
if logfile.exists():
|
||||
@ -204,6 +209,7 @@ class HomeAssistantCore(CoreSysAttributes):
|
||||
)
|
||||
await _update(rollback)
|
||||
else:
|
||||
self.sys_resolution.create_issue(IssueType.UPDATE_FAILED, ContextType.CORE)
|
||||
raise HomeAssistantUpdateError()
|
||||
|
||||
async def _start(self) -> None:
|
||||
|
@ -1,10 +1,17 @@
|
||||
"""Supervisor resolution center."""
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..resolution.const import UnsupportedReason
|
||||
from .const import SCHEDULED_HEALTHCHECK, IssueType, Suggestion
|
||||
from ..exceptions import ResolutionError, ResolutionNotFound
|
||||
from .const import (
|
||||
SCHEDULED_HEALTHCHECK,
|
||||
ContextType,
|
||||
IssueType,
|
||||
SuggestionType,
|
||||
UnsupportedReason,
|
||||
)
|
||||
from .data import Issue, Suggestion
|
||||
from .free_space import ResolutionStorage
|
||||
from .notify import ResolutionNotify
|
||||
|
||||
@ -19,9 +26,9 @@ class ResolutionManager(CoreSysAttributes):
|
||||
self.coresys: CoreSys = coresys
|
||||
self._notify = ResolutionNotify(coresys)
|
||||
self._storage = ResolutionStorage(coresys)
|
||||
self._dismissed_suggestions: List[Suggestion] = []
|
||||
|
||||
self._suggestions: List[Suggestion] = []
|
||||
self._issues: List[IssueType] = []
|
||||
self._issues: List[Issue] = []
|
||||
self._unsupported: List[UnsupportedReason] = []
|
||||
|
||||
@property
|
||||
@ -35,12 +42,12 @@ class ResolutionManager(CoreSysAttributes):
|
||||
return self._notify
|
||||
|
||||
@property
|
||||
def issues(self) -> List[IssueType]:
|
||||
def issues(self) -> List[Issue]:
|
||||
"""Return a list of issues."""
|
||||
return self._issues
|
||||
|
||||
@issues.setter
|
||||
def issues(self, issue: IssueType) -> None:
|
||||
def issues(self, issue: Issue) -> None:
|
||||
"""Add issues."""
|
||||
if issue not in self._issues:
|
||||
self._issues.append(issue)
|
||||
@ -48,7 +55,7 @@ class ResolutionManager(CoreSysAttributes):
|
||||
@property
|
||||
def suggestions(self) -> List[Suggestion]:
|
||||
"""Return a list of suggestions that can handled."""
|
||||
return [x for x in self._suggestions if x not in self._dismissed_suggestions]
|
||||
return self._suggestions
|
||||
|
||||
@suggestions.setter
|
||||
def suggestions(self, suggestion: Suggestion) -> None:
|
||||
@ -67,6 +74,38 @@ class ResolutionManager(CoreSysAttributes):
|
||||
if reason not in self._unsupported:
|
||||
self._unsupported.append(reason)
|
||||
|
||||
def get_suggestion(self, uuid: str) -> Suggestion:
|
||||
"""Return suggestion with uuid."""
|
||||
for suggestion in self._suggestions:
|
||||
if suggestion.uuid != uuid:
|
||||
continue
|
||||
return suggestion
|
||||
raise ResolutionNotFound()
|
||||
|
||||
def get_issue(self, uuid: str) -> Issue:
|
||||
"""Return issue with uuid."""
|
||||
for issue in self._issues:
|
||||
if issue.uuid != uuid:
|
||||
continue
|
||||
return issue
|
||||
raise ResolutionNotFound()
|
||||
|
||||
def create_issue(
|
||||
self,
|
||||
issue: IssueType,
|
||||
context: ContextType,
|
||||
reference: Optional[str] = None,
|
||||
suggestions: Optional[List[SuggestionType]] = None,
|
||||
) -> None:
|
||||
"""Create issues and suggestion."""
|
||||
self.issues = Issue(issue, context, reference)
|
||||
if not suggestions:
|
||||
return
|
||||
|
||||
# Add suggestions
|
||||
for suggestion in suggestions:
|
||||
self.suggestions = Suggestion(suggestion, context, reference)
|
||||
|
||||
async def load(self):
|
||||
"""Load the resoulution manager."""
|
||||
# Initial healthcheck when the manager is loaded
|
||||
@ -85,14 +124,14 @@ class ResolutionManager(CoreSysAttributes):
|
||||
|
||||
async def apply_suggestion(self, suggestion: Suggestion) -> None:
|
||||
"""Apply suggested action."""
|
||||
if suggestion not in self.suggestions:
|
||||
_LOGGER.warning("Suggestion %s is not valid", suggestion)
|
||||
return
|
||||
if suggestion not in self._suggestions:
|
||||
_LOGGER.warning("Suggestion %s is not valid", suggestion.uuid)
|
||||
raise ResolutionError()
|
||||
|
||||
if suggestion == Suggestion.CLEAR_FULL_SNAPSHOT:
|
||||
if suggestion.type == SuggestionType.CLEAR_FULL_SNAPSHOT:
|
||||
self.storage.clean_full_snapshots()
|
||||
|
||||
elif suggestion == Suggestion.CREATE_FULL_SNAPSHOT:
|
||||
elif suggestion.type == SuggestionType.CREATE_FULL_SNAPSHOT:
|
||||
await self.sys_snapshots.do_snapshot_full()
|
||||
|
||||
self._suggestions.remove(suggestion)
|
||||
@ -100,9 +139,14 @@ class ResolutionManager(CoreSysAttributes):
|
||||
|
||||
async def dismiss_suggestion(self, suggestion: Suggestion) -> None:
|
||||
"""Dismiss suggested action."""
|
||||
if suggestion not in self.suggestions:
|
||||
_LOGGER.warning("Suggestion %s is not valid", suggestion)
|
||||
return
|
||||
if suggestion not in self._suggestions:
|
||||
_LOGGER.warning("The UUID %s is not valid suggestion", suggestion.uuid)
|
||||
raise ResolutionError()
|
||||
self._suggestions.remove(suggestion)
|
||||
|
||||
if suggestion not in self._dismissed_suggestions:
|
||||
self._dismissed_suggestions.append(suggestion)
|
||||
async 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)
|
||||
|
@ -7,6 +7,16 @@ MINIMUM_FREE_SPACE_THRESHOLD = 1
|
||||
MINIMUM_FULL_SNAPSHOTS = 2
|
||||
|
||||
|
||||
class ContextType(str, Enum):
|
||||
"""Place where somethings was happening."""
|
||||
|
||||
SYSTEM = "system"
|
||||
SUPERVISOR = "supervisor"
|
||||
ADDON = "addon"
|
||||
CORE = "core"
|
||||
OS = "os"
|
||||
|
||||
|
||||
class UnsupportedReason(str, Enum):
|
||||
"""Reasons for unsupported status."""
|
||||
|
||||
@ -25,10 +35,15 @@ class IssueType(str, Enum):
|
||||
"""Issue type."""
|
||||
|
||||
FREE_SPACE = "free_space"
|
||||
CORRUPT_DOCKER = "corrupt_docker"
|
||||
MISSING_IMAGE = "missing_image"
|
||||
UPDATE_FAILED = "update_failed"
|
||||
UPDATE_ROLLBACK = "update_rollback"
|
||||
|
||||
|
||||
class Suggestion(str, Enum):
|
||||
"""Sugestion."""
|
||||
class SuggestionType(str, Enum):
|
||||
"""Sugestion type."""
|
||||
|
||||
CLEAR_FULL_SNAPSHOT = "clear_full_snapshot"
|
||||
CREATE_FULL_SNAPSHOT = "create_full_snapshot"
|
||||
SYSTEM_REPAIR = "system_repair"
|
||||
|
27
supervisor/resolution/data.py
Normal file
27
supervisor/resolution/data.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Data objects."""
|
||||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import attr
|
||||
|
||||
from .const import ContextType, IssueType, SuggestionType
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True)
|
||||
class Issue:
|
||||
"""Represent an Issue."""
|
||||
|
||||
type: IssueType = attr.ib()
|
||||
context: ContextType = attr.ib()
|
||||
reference: Optional[str] = attr.ib(default=None)
|
||||
uuid: UUID = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False)
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True)
|
||||
class Suggestion:
|
||||
"""Represent an Suggestion."""
|
||||
|
||||
type: SuggestionType = attr.ib()
|
||||
context: ContextType = attr.ib()
|
||||
reference: Optional[str] = attr.ib(default=None)
|
||||
uuid: UUID = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False)
|
@ -1,14 +1,17 @@
|
||||
"""Helpers to check and fix issues with free space."""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from ..const import SNAPSHOT_FULL
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from .const import (
|
||||
MINIMUM_FREE_SPACE_THRESHOLD,
|
||||
MINIMUM_FULL_SNAPSHOTS,
|
||||
ContextType,
|
||||
IssueType,
|
||||
Suggestion,
|
||||
SuggestionType,
|
||||
)
|
||||
from .data import Suggestion
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -23,10 +26,14 @@ class ResolutionStorage(CoreSysAttributes):
|
||||
def check_free_space(self) -> None:
|
||||
"""Check free space."""
|
||||
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!
|
||||
self.sys_resolution.suggestions = Suggestion(
|
||||
SuggestionType.CREATE_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
return
|
||||
|
||||
self.sys_resolution.issues = IssueType.FREE_SPACE
|
||||
|
||||
suggestions: List[SuggestionType] = []
|
||||
if (
|
||||
len(
|
||||
[
|
||||
@ -37,11 +44,11 @@ class ResolutionStorage(CoreSysAttributes):
|
||||
)
|
||||
>= MINIMUM_FULL_SNAPSHOTS
|
||||
):
|
||||
self.sys_resolution.suggestions = Suggestion.CLEAR_FULL_SNAPSHOT
|
||||
suggestions.append(SuggestionType.CLEAR_FULL_SNAPSHOT)
|
||||
|
||||
elif len(self.sys_snapshots.list_snapshots) == 0:
|
||||
# No snapshots, let's suggest the user to create one!
|
||||
self.sys_resolution.suggestions = Suggestion.CREATE_FULL_SNAPSHOT
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.FREE_SPACE, ContextType.SYSTEM, suggestions=suggestions
|
||||
)
|
||||
|
||||
def clean_full_snapshots(self):
|
||||
"""Clean out all old full snapshots, but keep the most recent."""
|
||||
|
@ -28,11 +28,11 @@ class ResolutionNotify(CoreSysAttributes):
|
||||
):
|
||||
return
|
||||
|
||||
issues = []
|
||||
messages = []
|
||||
|
||||
for issue in self.sys_resolution.issues:
|
||||
if issue == IssueType.FREE_SPACE:
|
||||
issues.append(
|
||||
if issue.type == IssueType.FREE_SPACE:
|
||||
messages.append(
|
||||
{
|
||||
"title": "Available space is less than 1GB!",
|
||||
"message": f"Available space is {self.sys_host.info.free_space}GB, see https://www.home-assistant.io/more-info/free-space for more information.",
|
||||
@ -40,12 +40,12 @@ class ResolutionNotify(CoreSysAttributes):
|
||||
}
|
||||
)
|
||||
|
||||
for issue in issues:
|
||||
for message in messages:
|
||||
try:
|
||||
async with self.sys_homeassistant.api.make_request(
|
||||
"post",
|
||||
"api/services/persistent_notification/create",
|
||||
json=issue,
|
||||
json=message,
|
||||
) as resp:
|
||||
if resp.status in (200, 201):
|
||||
_LOGGER.debug("Sucessfully created persistent_notification")
|
||||
|
@ -19,6 +19,7 @@ from .exceptions import (
|
||||
SupervisorError,
|
||||
SupervisorUpdateError,
|
||||
)
|
||||
from .resolution.const import ContextType, IssueType
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -117,6 +118,9 @@ class Supervisor(CoreSysAttributes):
|
||||
)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Update of Supervisor failed!")
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.UPDATE_FAILED, ContextType.SUPERVISOR
|
||||
)
|
||||
raise SupervisorUpdateError() from err
|
||||
else:
|
||||
self.sys_config.version = version
|
||||
|
@ -1,47 +1,86 @@
|
||||
"""Test Resolution API."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.const import ATTR_ISSUES, ATTR_SUGGESTIONS, ATTR_UNSUPPORTED
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.const import IssueType, Suggestion, UnsupportedReason
|
||||
from supervisor.exceptions import ResolutionError
|
||||
from supervisor.resolution.const import (
|
||||
ContextType,
|
||||
IssueType,
|
||||
SuggestionType,
|
||||
UnsupportedReason,
|
||||
)
|
||||
from supervisor.resolution.data import Issue, Suggestion
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_resolution_base(coresys: CoreSys, api_client):
|
||||
"""Test resolution manager api."""
|
||||
coresys.resolution.unsupported = UnsupportedReason.OS
|
||||
coresys.resolution.suggestions = Suggestion.CLEAR_FULL_SNAPSHOT
|
||||
coresys.resolution.issues = IssueType.FREE_SPACE
|
||||
coresys.resolution.suggestions = Suggestion(
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
coresys.resolution.create_issue(IssueType.FREE_SPACE, ContextType.SYSTEM)
|
||||
|
||||
resp = await api_client.get("/resolution")
|
||||
result = await resp.json()
|
||||
assert UnsupportedReason.OS in result["data"][ATTR_UNSUPPORTED]
|
||||
assert Suggestion.CLEAR_FULL_SNAPSHOT in result["data"][ATTR_SUGGESTIONS]
|
||||
assert IssueType.FREE_SPACE in result["data"][ATTR_ISSUES]
|
||||
assert (
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT
|
||||
== result["data"][ATTR_SUGGESTIONS][-1]["type"]
|
||||
)
|
||||
assert IssueType.FREE_SPACE == result["data"][ATTR_ISSUES][-1]["type"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_resolution_dismiss_suggestion(coresys: CoreSys, api_client):
|
||||
"""Test resolution manager suggestion apply api."""
|
||||
coresys.resolution.suggestions = Suggestion.CLEAR_FULL_SNAPSHOT
|
||||
coresys.resolution.suggestions = clear_snapshot = Suggestion(
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
assert Suggestion.CLEAR_FULL_SNAPSHOT in coresys.resolution.suggestions
|
||||
await coresys.resolution.dismiss_suggestion(Suggestion.CLEAR_FULL_SNAPSHOT)
|
||||
assert Suggestion.CLEAR_FULL_SNAPSHOT not in coresys.resolution.suggestions
|
||||
assert SuggestionType.CLEAR_FULL_SNAPSHOT == coresys.resolution.suggestions[-1].type
|
||||
await api_client.delete(f"/resolution/suggestion/{clear_snapshot.uuid}")
|
||||
assert clear_snapshot not in coresys.resolution.suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_resolution_apply_suggestion(coresys: CoreSys, api_client):
|
||||
"""Test resolution manager suggestion apply api."""
|
||||
coresys.resolution.suggestions = Suggestion.CLEAR_FULL_SNAPSHOT
|
||||
coresys.resolution.suggestions = Suggestion.CREATE_FULL_SNAPSHOT
|
||||
coresys.resolution.suggestions = clear_snapshot = Suggestion(
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
coresys.resolution.suggestions = create_snapshot = Suggestion(
|
||||
SuggestionType.CREATE_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
with patch("supervisor.snapshots.SnapshotManager", return_value=MagicMock()):
|
||||
await coresys.resolution.apply_suggestion(Suggestion.CLEAR_FULL_SNAPSHOT)
|
||||
await coresys.resolution.apply_suggestion(Suggestion.CREATE_FULL_SNAPSHOT)
|
||||
mock_snapshots = AsyncMock()
|
||||
mock_health = AsyncMock()
|
||||
coresys.snapshots.do_snapshot_full = mock_snapshots
|
||||
coresys.resolution.healthcheck = mock_health
|
||||
|
||||
assert Suggestion.CLEAR_FULL_SNAPSHOT not in coresys.resolution.suggestions
|
||||
assert Suggestion.CREATE_FULL_SNAPSHOT not in coresys.resolution.suggestions
|
||||
await api_client.post(f"/resolution/suggestion/{clear_snapshot.uuid}")
|
||||
await api_client.post(f"/resolution/suggestion/{create_snapshot.uuid}")
|
||||
|
||||
await coresys.resolution.apply_suggestion(Suggestion.CLEAR_FULL_SNAPSHOT)
|
||||
assert clear_snapshot not in coresys.resolution.suggestions
|
||||
assert create_snapshot not in coresys.resolution.suggestions
|
||||
|
||||
assert mock_snapshots.called
|
||||
assert mock_health.called
|
||||
|
||||
with pytest.raises(ResolutionError):
|
||||
await coresys.resolution.apply_suggestion(clear_snapshot)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_resolution_dismiss_issue(coresys: CoreSys, api_client):
|
||||
"""Test resolution manager issue apply api."""
|
||||
coresys.resolution.issues = updated_failed = Issue(
|
||||
IssueType.UPDATE_FAILED, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
assert IssueType.UPDATE_FAILED == coresys.resolution.issues[-1].type
|
||||
await api_client.delete(f"/resolution/issue/{updated_failed.uuid}")
|
||||
assert updated_failed not in coresys.resolution.issues
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Tests for resolution manager."""
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.const import (
|
||||
ATTR_DATE,
|
||||
@ -9,7 +12,14 @@ from supervisor.const import (
|
||||
SNAPSHOT_PARTIAL,
|
||||
)
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.const import UnsupportedReason
|
||||
from supervisor.exceptions import ResolutionError
|
||||
from supervisor.resolution.const import (
|
||||
ContextType,
|
||||
IssueType,
|
||||
SuggestionType,
|
||||
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
|
||||
@ -58,3 +68,73 @@ async def test_clear_snapshots(coresys: CoreSys, tmp_path):
|
||||
)
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolution_dismiss_suggestion(coresys: CoreSys):
|
||||
"""Test resolution manager suggestion apply api."""
|
||||
coresys.resolution.suggestions = clear_snapshot = Suggestion(
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
assert SuggestionType.CLEAR_FULL_SNAPSHOT == coresys.resolution.suggestions[-1].type
|
||||
await coresys.resolution.dismiss_suggestion(clear_snapshot)
|
||||
assert clear_snapshot not in coresys.resolution.suggestions
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolution_apply_suggestion(coresys: CoreSys):
|
||||
"""Test resolution manager suggestion apply api."""
|
||||
coresys.resolution.suggestions = clear_snapshot = Suggestion(
|
||||
SuggestionType.CLEAR_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
coresys.resolution.suggestions = create_snapshot = Suggestion(
|
||||
SuggestionType.CREATE_FULL_SNAPSHOT, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
mock_snapshots = AsyncMock()
|
||||
mock_health = AsyncMock()
|
||||
coresys.snapshots.do_snapshot_full = mock_snapshots
|
||||
coresys.resolution.healthcheck = mock_health
|
||||
|
||||
await coresys.resolution.apply_suggestion(clear_snapshot)
|
||||
await coresys.resolution.apply_suggestion(create_snapshot)
|
||||
|
||||
assert mock_snapshots.called
|
||||
assert mock_health.called
|
||||
|
||||
assert clear_snapshot not in coresys.resolution.suggestions
|
||||
assert create_snapshot not in coresys.resolution.suggestions
|
||||
|
||||
with pytest.raises(ResolutionError):
|
||||
await coresys.resolution.apply_suggestion(clear_snapshot)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolution_dismiss_issue(coresys: CoreSys):
|
||||
"""Test resolution manager issue apply api."""
|
||||
coresys.resolution.issues = updated_failed = Issue(
|
||||
IssueType.UPDATE_FAILED, ContextType.SYSTEM
|
||||
)
|
||||
|
||||
assert IssueType.UPDATE_FAILED == coresys.resolution.issues[-1].type
|
||||
await coresys.resolution.dismiss_issue(updated_failed)
|
||||
assert updated_failed not in coresys.resolution.issues
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolution_create_issue_suggestion(coresys: CoreSys):
|
||||
"""Test resolution manager issue and suggestion."""
|
||||
coresys.resolution.create_issue(
|
||||
IssueType.UPDATE_ROLLBACK,
|
||||
ContextType.CORE,
|
||||
"slug",
|
||||
[SuggestionType.SYSTEM_REPAIR],
|
||||
)
|
||||
|
||||
assert IssueType.UPDATE_ROLLBACK == coresys.resolution.issues[-1].type
|
||||
assert ContextType.CORE == coresys.resolution.issues[-1].context
|
||||
assert coresys.resolution.issues[-1].reference == "slug"
|
||||
|
||||
assert SuggestionType.SYSTEM_REPAIR == coresys.resolution.suggestions[-1].type
|
||||
assert ContextType.CORE == coresys.resolution.suggestions[-1].context
|
||||
|
Loading…
x
Reference in New Issue
Block a user