diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 81dcd7cb0..c09b83eb8 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -295,7 +295,18 @@ class ResolutionNotFound(ResolutionError): """Raise if suggestion/issue was not found.""" -# Job +# Store + + +class StoreError(HassioError): + """Raise if an error on store is happening.""" + + +class StoreGitError(StoreError): + """Raise if something on git is happening.""" + + +# JobManager class JobException(HassioError): diff --git a/supervisor/resolution/const.py b/supervisor/resolution/const.py index 696ef0065..334e4434b 100644 --- a/supervisor/resolution/const.py +++ b/supervisor/resolution/const.py @@ -10,12 +10,13 @@ MINIMUM_FULL_SNAPSHOTS = 2 class ContextType(str, Enum): """Place where somethings was happening.""" - SYSTEM = "system" - SUPERVISOR = "supervisor" - PLUGIN = "plugin" ADDON = "addon" CORE = "core" OS = "os" + PLUGIN = "plugin" + SUPERVISOR = "supervisor" + STORE = "store" + SYSTEM = "system" class UnsupportedReason(str, Enum): @@ -37,6 +38,7 @@ class IssueType(str, Enum): FREE_SPACE = "free_space" CORRUPT_DOCKER = "corrupt_docker" + CORRUPT_REPOSITORY = "corrupt_repository" MISSING_IMAGE = "missing_image" UPDATE_FAILED = "update_failed" UPDATE_ROLLBACK = "update_rollback" @@ -52,3 +54,5 @@ class SuggestionType(str, Enum): EXECUTE_UPDATE = "execute_update" EXECUTE_REPAIR = "execute_repair" EXECUTE_RESET = "execute_reset" + EXECUTE_RELOAD = "execute_reload" + NEW_INITIALIZE = "new_initialize" diff --git a/supervisor/store/__init__.py b/supervisor/store/__init__.py index 8bdeaf09a..956383280 100644 --- a/supervisor/store/__init__.py +++ b/supervisor/store/__init__.py @@ -11,7 +11,7 @@ from supervisor.utils.json import read_json_file from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL from ..coresys import CoreSys, CoreSysAttributes -from ..exceptions import JsonFileError +from ..exceptions import JsonFileError, StoreError, StoreGitError from ..job.decorator import Job, JobCondition from .addon import AddonStore from .data import StoreData @@ -70,9 +70,11 @@ class StoreManager(CoreSysAttributes): progress=job.progress + step, stage=f"Checking {url} started" ) repository = Repository(self.coresys, url) - if not await repository.load(): + try: + await repository.load() + except StoreGitError as err: _LOGGER.error("Can't load data from repository %s", url) - return + raise StoreError() from err # don't add built-in repository to config if url not in BUILTIN_REPOSITORIES: diff --git a/supervisor/store/git.py b/supervisor/store/git.py index ca184678a..0c113a7fb 100644 --- a/supervisor/store/git.py +++ b/supervisor/store/git.py @@ -4,11 +4,14 @@ import functools as ft import logging from pathlib import Path import shutil +from typing import Dict, Optional import git from ..const import ATTR_BRANCH, ATTR_URL, URL_HASSIO_ADDONS -from ..coresys import CoreSysAttributes +from ..coresys import CoreSys, CoreSysAttributes +from ..exceptions import StoreGitError +from ..resolution.const import ContextType, IssueType, SuggestionType from ..validate import RE_REPOSITORY from .utils import get_hash_from_repository @@ -18,30 +21,33 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) class GitRepo(CoreSysAttributes): """Manage Add-on Git repository.""" - def __init__(self, coresys, path, url): + def __init__(self, coresys: CoreSys, path: Path, url: str): """Initialize Git base wrapper.""" - self.coresys = coresys - self.repo = None - self.path = path - self.lock = asyncio.Lock() + self.coresys: CoreSys = coresys + self.repo: Optional[git.Repo] = None + self.path: Path = path + self.lock: asyncio.Lock = asyncio.Lock() - self.data = RE_REPOSITORY.match(url).groupdict() + self.data: Dict[str, str] = RE_REPOSITORY.match(url).groupdict() + self.slug: str = url @property - def url(self): + def url(self) -> str: """Return repository URL.""" return self.data[ATTR_URL] @property - def branch(self): + def branch(self) -> str: """Return repository branch.""" return self.data[ATTR_BRANCH] - async def load(self): + async def load(self) -> None: """Init Git add-on repository.""" if not self.path.is_dir(): - return await self.clone() + await self.clone() + return + # Load repository async with self.lock: try: _LOGGER.info("Loading add-on %s repository", self.path) @@ -53,12 +59,29 @@ class GitRepo(CoreSysAttributes): git.GitCommandError, ) as err: _LOGGER.error("Can't load %s repo: %s.", self.path, err) - await self._remove() - return False + self.sys_resolution.create_issue( + IssueType.FATAL_ERROR, + ContextType.STORE, + reference=self.slug, + ) + raise StoreGitError() from err - return True + # Fix possible corruption + async with self.lock: + try: + _LOGGER.debug("Integrity check add-on %s repository", self.path) + await self.sys_run_in_executor(self.repo.git.execute, ["git", "fsck"]) + except git.GitCommandError as err: + _LOGGER.error("Integrity check on %s failed: %s.", self.path, err) + self.sys_resolution.create_issue( + IssueType.CORRUPT_REPOSITORY, + ContextType.STORE, + reference=self.slug, + suggestions=[SuggestionType.EXECUTE_RESET], + ) + raise StoreGitError() from err - async def clone(self): + async def clone(self) -> None: """Clone git add-on repository.""" async with self.lock: git_args = { @@ -86,16 +109,19 @@ class GitRepo(CoreSysAttributes): git.GitCommandError, ) as err: _LOGGER.error("Can't clone %s repository: %s.", self.url, err) - await self._remove() - return False - - return True + self.sys_resolution.create_issue( + IssueType.FATAL_ERROR, + ContextType.STORE, + reference=self.slug, + suggestions=[SuggestionType.NEW_INITIALIZE], + ) + raise StoreGitError() from err async def pull(self): """Pull Git add-on repo.""" if self.lock.locked(): _LOGGER.warning("There is already a task in progress") - return False + return async with self.lock: _LOGGER.info("Update add-on %s repository", self.url) @@ -124,9 +150,13 @@ class GitRepo(CoreSysAttributes): git.GitCommandError, ) as err: _LOGGER.error("Can't update %s repo: %s.", self.url, err) - return False - - return True + self.sys_resolution.create_issue( + IssueType.CORRUPT_REPOSITORY, + ContextType.STORE, + reference=self.slug, + suggestions=[SuggestionType.EXECUTE_RELOAD], + ) + raise StoreGitError() from err async def _remove(self): """Remove a repository.""" diff --git a/supervisor/store/repository.py b/supervisor/store/repository.py index 3077f949a..7a6019e6e 100644 --- a/supervisor/store/repository.py +++ b/supervisor/store/repository.py @@ -57,15 +57,15 @@ class Repository(CoreSysAttributes): async def load(self): """Load addon repository.""" - if self.git: - return await self.git.load() - return True + if not self.git: + return + await self.git.load() async def update(self): """Update add-on repository.""" - if self.git: - return await self.git.pull() - return True + if not self.git: + return + await self.git.pull() async def remove(self): """Remove add-on repository."""