From 0c55bf20fceb96f3974fc453dc1be58edebcaf0c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Dec 2020 21:06:48 +0100 Subject: [PATCH] Fix issue on store git clone (#2331) --- supervisor/bootstrap.py | 2 +- supervisor/core.py | 1 + supervisor/coresys.py | 2 +- supervisor/exceptions.py | 8 + supervisor/resolution/__init__.py | 182 +----------------- supervisor/resolution/fixup.py | 5 +- .../resolution/fixups/store_execute_reset.py | 13 +- supervisor/resolution/module.py | 181 +++++++++++++++++ supervisor/store/__init__.py | 10 +- supervisor/store/git.py | 24 ++- supervisor/store/repository.py | 2 +- 11 files changed, 238 insertions(+), 192 deletions(-) create mode 100644 supervisor/resolution/module.py diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index f99207490..5b1ade822 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -44,7 +44,7 @@ from .misc.hwmon import HwMonitor from .misc.scheduler import Scheduler from .misc.tasks import Tasks from .plugins import PluginManager -from .resolution import ResolutionManager +from .resolution.module import ResolutionManager from .services import ServiceManager from .snapshots import SnapshotManager from .store import StoreManager diff --git a/supervisor/core.py b/supervisor/core.py index 10dd24b6a..68b975c27 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -83,6 +83,7 @@ class Core(CoreSysAttributes): """Start setting up supervisor orchestration.""" self.state = CoreState.SETUP + # Order can be important! setup_loads: List[Awaitable[None]] = [ # rest api views self.sys_api.load(), diff --git a/supervisor/coresys.py b/supervisor/coresys.py index 60db6a3bf..954fb46c7 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: from .misc.scheduler import Scheduler from .misc.tasks import Tasks from .plugins import PluginManager - from .resolution import ResolutionManager + from .resolution.module import ResolutionManager from .services import ServiceManager from .snapshots import SnapshotManager from .store import StoreManager diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 0d8507046..848e6a397 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -322,6 +322,10 @@ class ResolutionFixupError(HassioError): """Rasie if a fixup fails.""" +class ResolutionFixupJobError(ResolutionFixupError, JobException): + """Raise on job error.""" + + # Store @@ -335,3 +339,7 @@ class StoreGitError(StoreError): class StoreNotFound(StoreError): """Raise if slug is not known.""" + + +class StoreJobError(StoreError, JobException): + """Raise on job error with git.""" diff --git a/supervisor/resolution/__init__.py b/supervisor/resolution/__init__.py index cb1f85874..700244dda 100644 --- a/supervisor/resolution/__init__.py +++ b/supervisor/resolution/__init__.py @@ -1,181 +1 @@ -"""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, - IssueType, - SuggestionType, - UnhealthyReason, - UnsupportedReason, -) -from .data import Issue, Suggestion -from .evaluate import ResolutionEvaluation -from .fixup import ResolutionFixup -from .notify import ResolutionNotify - -_LOGGER: logging.Logger = logging.getLogger(__name__) - - -class ResolutionManager(CoreSysAttributes): - """Resolution manager for supervisor.""" - - def __init__(self, coresys: CoreSys): - """Initialize Resolution manager.""" - self.coresys: CoreSys = coresys - self._evaluate = ResolutionEvaluation(coresys) - self._check = ResolutionCheck(coresys) - self._fixup = ResolutionFixup(coresys) - self._notify = ResolutionNotify(coresys) - - self._suggestions: List[Suggestion] = [] - self._issues: List[Issue] = [] - self._unsupported: List[UnsupportedReason] = [] - self._unhealthy: List[UnhealthyReason] = [] - - @property - def evaluate(self) -> ResolutionEvaluation: - """Return the ResolutionEvaluation class.""" - return self._evaluate - - @property - 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: - """Return the ResolutionNotify class.""" - return self._notify - - @property - def issues(self) -> List[Issue]: - """Return a list of issues.""" - return self._issues - - @issues.setter - def issues(self, issue: Issue) -> None: - """Add issues.""" - if issue not in self._issues: - self._issues.append(issue) - - @property - def suggestions(self) -> List[Suggestion]: - """Return a list of suggestions that can handled.""" - return self._suggestions - - @suggestions.setter - def suggestions(self, suggestion: Suggestion) -> None: - """Add suggestion.""" - if suggestion not in self._suggestions: - self._suggestions.append(suggestion) - - @property - def unsupported(self) -> List[UnsupportedReason]: - """Return a list of unsupported reasons.""" - return self._unsupported - - @unsupported.setter - def unsupported(self, reason: UnsupportedReason) -> None: - """Add a reason for unsupported.""" - if reason not in self._unsupported: - self._unsupported.append(reason) - - @property - def unhealthy(self) -> List[UnhealthyReason]: - """Return a list of unsupported reasons.""" - return self._unhealthy - - @unhealthy.setter - def unhealthy(self, reason: UnhealthyReason) -> None: - """Add a reason for unsupported.""" - if reason not in self._unhealthy: - self._unhealthy.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 - await self.healthcheck() - - # 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.""" - await self.check.check_system() - - # Create notification for any known issues - await self.notify.issue_notifications() - - 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.uuid) - raise ResolutionError() - - await self.fixup.apply_fixup(suggestion) - await self.healthcheck() - - 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) - - 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) - - 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) - raise ResolutionError() - self._unsupported.remove(reason) +"""Resolution Supervisor module.""" diff --git a/supervisor/resolution/fixup.py b/supervisor/resolution/fixup.py index d82ece6fd..69d0713e7 100644 --- a/supervisor/resolution/fixup.py +++ b/supervisor/resolution/fixup.py @@ -29,7 +29,10 @@ class ResolutionFixup(CoreSysAttributes): @property def all_fixes(self) -> List[FixupBase]: - """Return a list of all fixups.""" + """Return a list of all fixups. + + Order can be important! + """ return [ self._create_full_snapshot, self._clear_full_snapshot, diff --git a/supervisor/resolution/fixups/store_execute_reset.py b/supervisor/resolution/fixups/store_execute_reset.py index 1a1829546..b9cdac00c 100644 --- a/supervisor/resolution/fixups/store_execute_reset.py +++ b/supervisor/resolution/fixups/store_execute_reset.py @@ -2,8 +2,14 @@ import logging from typing import List, Optional -from supervisor.exceptions import ResolutionFixupError, StoreError, StoreNotFound - +from ...exceptions import ( + ResolutionFixupError, + ResolutionFixupJobError, + StoreError, + StoreNotFound, +) +from ...jobs.const import JobCondition +from ...jobs.decorator import Job from ...utils import remove_folder from ..const import ContextType, IssueType, SuggestionType from .base import FixupBase @@ -14,6 +20,9 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class FixupStoreExecuteReset(FixupBase): """Storage class for fixup.""" + @Job( + conditions=[JobCondition.INTERNET_SYSTEM], on_condition=ResolutionFixupJobError + ) async def process_fixup(self, reference: Optional[str] = None) -> None: """Initialize the fixup class.""" _LOGGER.info("Reset corrupt Store: %s", reference) diff --git a/supervisor/resolution/module.py b/supervisor/resolution/module.py new file mode 100644 index 000000000..cb1f85874 --- /dev/null +++ b/supervisor/resolution/module.py @@ -0,0 +1,181 @@ +"""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, + IssueType, + SuggestionType, + UnhealthyReason, + UnsupportedReason, +) +from .data import Issue, Suggestion +from .evaluate import ResolutionEvaluation +from .fixup import ResolutionFixup +from .notify import ResolutionNotify + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class ResolutionManager(CoreSysAttributes): + """Resolution manager for supervisor.""" + + def __init__(self, coresys: CoreSys): + """Initialize Resolution manager.""" + self.coresys: CoreSys = coresys + self._evaluate = ResolutionEvaluation(coresys) + self._check = ResolutionCheck(coresys) + self._fixup = ResolutionFixup(coresys) + self._notify = ResolutionNotify(coresys) + + self._suggestions: List[Suggestion] = [] + self._issues: List[Issue] = [] + self._unsupported: List[UnsupportedReason] = [] + self._unhealthy: List[UnhealthyReason] = [] + + @property + def evaluate(self) -> ResolutionEvaluation: + """Return the ResolutionEvaluation class.""" + return self._evaluate + + @property + 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: + """Return the ResolutionNotify class.""" + return self._notify + + @property + def issues(self) -> List[Issue]: + """Return a list of issues.""" + return self._issues + + @issues.setter + def issues(self, issue: Issue) -> None: + """Add issues.""" + if issue not in self._issues: + self._issues.append(issue) + + @property + def suggestions(self) -> List[Suggestion]: + """Return a list of suggestions that can handled.""" + return self._suggestions + + @suggestions.setter + def suggestions(self, suggestion: Suggestion) -> None: + """Add suggestion.""" + if suggestion not in self._suggestions: + self._suggestions.append(suggestion) + + @property + def unsupported(self) -> List[UnsupportedReason]: + """Return a list of unsupported reasons.""" + return self._unsupported + + @unsupported.setter + def unsupported(self, reason: UnsupportedReason) -> None: + """Add a reason for unsupported.""" + if reason not in self._unsupported: + self._unsupported.append(reason) + + @property + def unhealthy(self) -> List[UnhealthyReason]: + """Return a list of unsupported reasons.""" + return self._unhealthy + + @unhealthy.setter + def unhealthy(self, reason: UnhealthyReason) -> None: + """Add a reason for unsupported.""" + if reason not in self._unhealthy: + self._unhealthy.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 + await self.healthcheck() + + # 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.""" + await self.check.check_system() + + # Create notification for any known issues + await self.notify.issue_notifications() + + 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.uuid) + raise ResolutionError() + + await self.fixup.apply_fixup(suggestion) + await self.healthcheck() + + 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) + + 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) + + 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) + raise ResolutionError() + self._unsupported.remove(reason) diff --git a/supervisor/store/__init__.py b/supervisor/store/__init__.py index ed2dd527e..c0e58402d 100644 --- a/supervisor/store/__init__.py +++ b/supervisor/store/__init__.py @@ -4,7 +4,7 @@ import logging from typing import Dict, List from ..coresys import CoreSys, CoreSysAttributes -from ..exceptions import StoreGitError, StoreNotFound +from ..exceptions import StoreGitError, StoreJobError, StoreNotFound from ..jobs.decorator import Job, JobCondition from ..resolution.const import ContextType, IssueType, SuggestionType from .addon import AddonStore @@ -83,6 +83,14 @@ class StoreManager(CoreSysAttributes): await repository.load() except StoreGitError: _LOGGER.error("Can't load data from repository %s", url) + except StoreJobError: + _LOGGER.warning("Skip update to later for %s", repository.slug) + self.sys_resolution.create_issue( + IssueType.FATAL_ERROR, + ContextType.STORE, + refrence=repository.slug, + suggestions=[SuggestionType.EXECUTE_RELOAD], + ) else: if not repository.validate(): _LOGGER.error("%s is not a valid add-on repository", url) diff --git a/supervisor/store/git.py b/supervisor/store/git.py index 11f76e984..34f234d6b 100644 --- a/supervisor/store/git.py +++ b/supervisor/store/git.py @@ -9,7 +9,7 @@ import git from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS from ..coresys import CoreSys, CoreSysAttributes -from ..exceptions import StoreGitError +from ..exceptions import StoreGitError, StoreJobError from ..jobs.decorator import Job, JobCondition from ..resolution.const import ContextType, IssueType, SuggestionType from ..utils import remove_folder @@ -22,6 +22,8 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class GitRepo(CoreSysAttributes): """Manage Add-on Git repository.""" + builtin: bool + def __init__(self, coresys: CoreSys, path: Path, url: str): """Initialize Git base wrapper.""" self.coresys: CoreSys = coresys @@ -82,7 +84,10 @@ class GitRepo(CoreSysAttributes): ) raise StoreGitError() from err - @Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM]) + @Job( + conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM], + on_condition=StoreJobError, + ) async def clone(self) -> None: """Clone git add-on repository.""" async with self.lock: @@ -115,11 +120,18 @@ class GitRepo(CoreSysAttributes): IssueType.FATAL_ERROR, ContextType.STORE, reference=self.path.stem, - suggestions=[SuggestionType.EXECUTE_RELOAD], + suggestions=[ + SuggestionType.EXECUTE_RELOAD + if self.builtin + else SuggestionType.EXECUTE_REMOVE + ], ) raise StoreGitError() from err - @Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM]) + @Job( + conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_SYSTEM], + on_condition=StoreJobError, + ) async def pull(self): """Pull Git add-on repo.""" if self.lock.locked(): @@ -175,6 +187,8 @@ class GitRepo(CoreSysAttributes): class GitRepoHassIO(GitRepo): """Supervisor add-ons repository.""" + builtin: bool = False + def __init__(self, coresys): """Initialize Git Supervisor add-on repository.""" super().__init__(coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS) @@ -183,6 +197,8 @@ class GitRepoHassIO(GitRepo): class GitRepoCustom(GitRepo): """Custom add-ons repository.""" + builtin: bool = False + def __init__(self, coresys, url): """Initialize custom Git Supervisor addo-n repository.""" path = Path(coresys.config.path_addons_git, get_hash_from_repository(url)) diff --git a/supervisor/store/repository.py b/supervisor/store/repository.py index 5269c05f1..18767b573 100644 --- a/supervisor/store/repository.py +++ b/supervisor/store/repository.py @@ -100,7 +100,7 @@ class Repository(CoreSysAttributes): async def update(self) -> None: """Update add-on repository.""" - if self.type == StoreType.LOCAL: + if self.type == StoreType.LOCAL or not self.validate(): return await self.git.pull()