Check & fix git repository integrity (#2245)

* Check & fix git repository integrity

* fix typing

* New style error handling

* Fix return

* tag more issue

* Fix black
This commit is contained in:
Pascal Vizeli 2020-11-13 08:35:45 +01:00 committed by GitHub
parent c209d2fa8d
commit e4bf820038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 83 additions and 36 deletions

View File

@ -295,7 +295,18 @@ class ResolutionNotFound(ResolutionError):
"""Raise if suggestion/issue was not found.""" """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): class JobException(HassioError):

View File

@ -10,12 +10,13 @@ MINIMUM_FULL_SNAPSHOTS = 2
class ContextType(str, Enum): class ContextType(str, Enum):
"""Place where somethings was happening.""" """Place where somethings was happening."""
SYSTEM = "system"
SUPERVISOR = "supervisor"
PLUGIN = "plugin"
ADDON = "addon" ADDON = "addon"
CORE = "core" CORE = "core"
OS = "os" OS = "os"
PLUGIN = "plugin"
SUPERVISOR = "supervisor"
STORE = "store"
SYSTEM = "system"
class UnsupportedReason(str, Enum): class UnsupportedReason(str, Enum):
@ -37,6 +38,7 @@ class IssueType(str, Enum):
FREE_SPACE = "free_space" FREE_SPACE = "free_space"
CORRUPT_DOCKER = "corrupt_docker" CORRUPT_DOCKER = "corrupt_docker"
CORRUPT_REPOSITORY = "corrupt_repository"
MISSING_IMAGE = "missing_image" MISSING_IMAGE = "missing_image"
UPDATE_FAILED = "update_failed" UPDATE_FAILED = "update_failed"
UPDATE_ROLLBACK = "update_rollback" UPDATE_ROLLBACK = "update_rollback"
@ -52,3 +54,5 @@ class SuggestionType(str, Enum):
EXECUTE_UPDATE = "execute_update" EXECUTE_UPDATE = "execute_update"
EXECUTE_REPAIR = "execute_repair" EXECUTE_REPAIR = "execute_repair"
EXECUTE_RESET = "execute_reset" EXECUTE_RESET = "execute_reset"
EXECUTE_RELOAD = "execute_reload"
NEW_INITIALIZE = "new_initialize"

View File

@ -11,7 +11,7 @@ from supervisor.utils.json import read_json_file
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import JsonFileError from ..exceptions import JsonFileError, StoreError, StoreGitError
from ..job.decorator import Job, JobCondition from ..job.decorator import Job, JobCondition
from .addon import AddonStore from .addon import AddonStore
from .data import StoreData from .data import StoreData
@ -70,9 +70,11 @@ class StoreManager(CoreSysAttributes):
progress=job.progress + step, stage=f"Checking {url} started" progress=job.progress + step, stage=f"Checking {url} started"
) )
repository = Repository(self.coresys, url) 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) _LOGGER.error("Can't load data from repository %s", url)
return raise StoreError() from err
# don't add built-in repository to config # don't add built-in repository to config
if url not in BUILTIN_REPOSITORIES: if url not in BUILTIN_REPOSITORIES:

View File

@ -4,11 +4,14 @@ import functools as ft
import logging import logging
from pathlib import Path from pathlib import Path
import shutil import shutil
from typing import Dict, Optional
import git 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 CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import StoreGitError
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..validate import RE_REPOSITORY from ..validate import RE_REPOSITORY
from .utils import get_hash_from_repository from .utils import get_hash_from_repository
@ -18,30 +21,33 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
class GitRepo(CoreSysAttributes): class GitRepo(CoreSysAttributes):
"""Manage Add-on Git repository.""" """Manage Add-on Git repository."""
def __init__(self, coresys, path, url): def __init__(self, coresys: CoreSys, path: Path, url: str):
"""Initialize Git base wrapper.""" """Initialize Git base wrapper."""
self.coresys = coresys self.coresys: CoreSys = coresys
self.repo = None self.repo: Optional[git.Repo] = None
self.path = path self.path: Path = path
self.lock = asyncio.Lock() 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 @property
def url(self): def url(self) -> str:
"""Return repository URL.""" """Return repository URL."""
return self.data[ATTR_URL] return self.data[ATTR_URL]
@property @property
def branch(self): def branch(self) -> str:
"""Return repository branch.""" """Return repository branch."""
return self.data[ATTR_BRANCH] return self.data[ATTR_BRANCH]
async def load(self): async def load(self) -> None:
"""Init Git add-on repository.""" """Init Git add-on repository."""
if not self.path.is_dir(): if not self.path.is_dir():
return await self.clone() await self.clone()
return
# Load repository
async with self.lock: async with self.lock:
try: try:
_LOGGER.info("Loading add-on %s repository", self.path) _LOGGER.info("Loading add-on %s repository", self.path)
@ -53,12 +59,29 @@ class GitRepo(CoreSysAttributes):
git.GitCommandError, git.GitCommandError,
) as err: ) as err:
_LOGGER.error("Can't load %s repo: %s.", self.path, err) _LOGGER.error("Can't load %s repo: %s.", self.path, err)
await self._remove() self.sys_resolution.create_issue(
return False 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.""" """Clone git add-on repository."""
async with self.lock: async with self.lock:
git_args = { git_args = {
@ -86,16 +109,19 @@ class GitRepo(CoreSysAttributes):
git.GitCommandError, git.GitCommandError,
) as err: ) as err:
_LOGGER.error("Can't clone %s repository: %s.", self.url, err) _LOGGER.error("Can't clone %s repository: %s.", self.url, err)
await self._remove() self.sys_resolution.create_issue(
return False IssueType.FATAL_ERROR,
ContextType.STORE,
return True reference=self.slug,
suggestions=[SuggestionType.NEW_INITIALIZE],
)
raise StoreGitError() from err
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():
_LOGGER.warning("There is already a task in progress") _LOGGER.warning("There is already a task in progress")
return False return
async with self.lock: async with self.lock:
_LOGGER.info("Update add-on %s repository", self.url) _LOGGER.info("Update add-on %s repository", self.url)
@ -124,9 +150,13 @@ class GitRepo(CoreSysAttributes):
git.GitCommandError, git.GitCommandError,
) as err: ) as err:
_LOGGER.error("Can't update %s repo: %s.", self.url, err) _LOGGER.error("Can't update %s repo: %s.", self.url, err)
return False self.sys_resolution.create_issue(
IssueType.CORRUPT_REPOSITORY,
return True ContextType.STORE,
reference=self.slug,
suggestions=[SuggestionType.EXECUTE_RELOAD],
)
raise StoreGitError() from err
async def _remove(self): async def _remove(self):
"""Remove a repository.""" """Remove a repository."""

View File

@ -57,15 +57,15 @@ class Repository(CoreSysAttributes):
async def load(self): async def load(self):
"""Load addon repository.""" """Load addon repository."""
if self.git: if not self.git:
return await self.git.load() return
return True await self.git.load()
async def update(self): async def update(self):
"""Update add-on repository.""" """Update add-on repository."""
if self.git: if not self.git:
return await self.git.pull() return
return True await self.git.pull()
async def remove(self): async def remove(self):
"""Remove add-on repository.""" """Remove add-on repository."""