Fix issue on store git clone (#2331)

This commit is contained in:
Pascal Vizeli 2020-12-03 21:06:48 +01:00 committed by GitHub
parent f8fd7b5933
commit 0c55bf20fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 238 additions and 192 deletions

View File

@ -44,7 +44,7 @@ from .misc.hwmon import HwMonitor
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
from .misc.tasks import Tasks from .misc.tasks import Tasks
from .plugins import PluginManager from .plugins import PluginManager
from .resolution import ResolutionManager from .resolution.module import ResolutionManager
from .services import ServiceManager from .services import ServiceManager
from .snapshots import SnapshotManager from .snapshots import SnapshotManager
from .store import StoreManager from .store import StoreManager

View File

@ -83,6 +83,7 @@ class Core(CoreSysAttributes):
"""Start setting up supervisor orchestration.""" """Start setting up supervisor orchestration."""
self.state = CoreState.SETUP self.state = CoreState.SETUP
# Order can be important!
setup_loads: List[Awaitable[None]] = [ setup_loads: List[Awaitable[None]] = [
# rest api views # rest api views
self.sys_api.load(), self.sys_api.load(),

View File

@ -30,7 +30,7 @@ if TYPE_CHECKING:
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
from .misc.tasks import Tasks from .misc.tasks import Tasks
from .plugins import PluginManager from .plugins import PluginManager
from .resolution import ResolutionManager from .resolution.module import ResolutionManager
from .services import ServiceManager from .services import ServiceManager
from .snapshots import SnapshotManager from .snapshots import SnapshotManager
from .store import StoreManager from .store import StoreManager

View File

@ -322,6 +322,10 @@ class ResolutionFixupError(HassioError):
"""Rasie if a fixup fails.""" """Rasie if a fixup fails."""
class ResolutionFixupJobError(ResolutionFixupError, JobException):
"""Raise on job error."""
# Store # Store
@ -335,3 +339,7 @@ class StoreGitError(StoreError):
class StoreNotFound(StoreError): class StoreNotFound(StoreError):
"""Raise if slug is not known.""" """Raise if slug is not known."""
class StoreJobError(StoreError, JobException):
"""Raise on job error with git."""

View File

@ -1,181 +1 @@
"""Supervisor resolution center.""" """Resolution Supervisor module."""
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)

View File

@ -29,7 +29,10 @@ class ResolutionFixup(CoreSysAttributes):
@property @property
def all_fixes(self) -> List[FixupBase]: def all_fixes(self) -> List[FixupBase]:
"""Return a list of all fixups.""" """Return a list of all fixups.
Order can be important!
"""
return [ return [
self._create_full_snapshot, self._create_full_snapshot,
self._clear_full_snapshot, self._clear_full_snapshot,

View File

@ -2,8 +2,14 @@
import logging import logging
from typing import List, Optional 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 ...utils import remove_folder
from ..const import ContextType, IssueType, SuggestionType from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase from .base import FixupBase
@ -14,6 +20,9 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class FixupStoreExecuteReset(FixupBase): class FixupStoreExecuteReset(FixupBase):
"""Storage class for fixup.""" """Storage class for fixup."""
@Job(
conditions=[JobCondition.INTERNET_SYSTEM], on_condition=ResolutionFixupJobError
)
async def process_fixup(self, reference: Optional[str] = None) -> None: async def process_fixup(self, reference: Optional[str] = None) -> None:
"""Initialize the fixup class.""" """Initialize the fixup class."""
_LOGGER.info("Reset corrupt Store: %s", reference) _LOGGER.info("Reset corrupt Store: %s", reference)

View File

@ -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)

View File

@ -4,7 +4,7 @@ import logging
from typing import Dict, List from typing import Dict, List
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import StoreGitError, StoreNotFound from ..exceptions import StoreGitError, StoreJobError, StoreNotFound
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from .addon import AddonStore from .addon import AddonStore
@ -83,6 +83,14 @@ class StoreManager(CoreSysAttributes):
await repository.load() await repository.load()
except StoreGitError: except StoreGitError:
_LOGGER.error("Can't load data from repository %s", url) _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: else:
if not repository.validate(): if not repository.validate():
_LOGGER.error("%s is not a valid add-on repository", url) _LOGGER.error("%s is not a valid add-on repository", url)

View File

@ -9,7 +9,7 @@ import git
from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import StoreGitError from ..exceptions import StoreGitError, StoreJobError
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import remove_folder from ..utils import remove_folder
@ -22,6 +22,8 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class GitRepo(CoreSysAttributes): class GitRepo(CoreSysAttributes):
"""Manage Add-on Git repository.""" """Manage Add-on Git repository."""
builtin: bool
def __init__(self, coresys: CoreSys, path: Path, url: str): def __init__(self, coresys: CoreSys, path: Path, url: str):
"""Initialize Git base wrapper.""" """Initialize Git base wrapper."""
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
@ -82,7 +84,10 @@ class GitRepo(CoreSysAttributes):
) )
raise StoreGitError() from err 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: async def clone(self) -> None:
"""Clone git add-on repository.""" """Clone git add-on repository."""
async with self.lock: async with self.lock:
@ -115,11 +120,18 @@ class GitRepo(CoreSysAttributes):
IssueType.FATAL_ERROR, IssueType.FATAL_ERROR,
ContextType.STORE, ContextType.STORE,
reference=self.path.stem, reference=self.path.stem,
suggestions=[SuggestionType.EXECUTE_RELOAD], suggestions=[
SuggestionType.EXECUTE_RELOAD
if self.builtin
else SuggestionType.EXECUTE_REMOVE
],
) )
raise StoreGitError() from err 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): async def pull(self):
"""Pull Git add-on repo.""" """Pull Git add-on repo."""
if self.lock.locked(): if self.lock.locked():
@ -175,6 +187,8 @@ class GitRepo(CoreSysAttributes):
class GitRepoHassIO(GitRepo): class GitRepoHassIO(GitRepo):
"""Supervisor add-ons repository.""" """Supervisor add-ons repository."""
builtin: bool = False
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize Git Supervisor add-on repository.""" """Initialize Git Supervisor add-on repository."""
super().__init__(coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS) super().__init__(coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS)
@ -183,6 +197,8 @@ class GitRepoHassIO(GitRepo):
class GitRepoCustom(GitRepo): class GitRepoCustom(GitRepo):
"""Custom add-ons repository.""" """Custom add-ons repository."""
builtin: bool = False
def __init__(self, coresys, url): def __init__(self, coresys, url):
"""Initialize custom Git Supervisor addo-n repository.""" """Initialize custom Git Supervisor addo-n repository."""
path = Path(coresys.config.path_addons_git, get_hash_from_repository(url)) path = Path(coresys.config.path_addons_git, get_hash_from_repository(url))

View File

@ -100,7 +100,7 @@ class Repository(CoreSysAttributes):
async def update(self) -> None: async def update(self) -> None:
"""Update add-on repository.""" """Update add-on repository."""
if self.type == StoreType.LOCAL: if self.type == StoreType.LOCAL or not self.validate():
return return
await self.git.pull() await self.git.pull()