Mini Shell
"""Custom restic error subclasses"""
import signal
import logging
from typing import TYPE_CHECKING, Union
from subprocess import CalledProcessError, CompletedProcess
import re
import os
if TYPE_CHECKING:
from .base import Restic
class ResticError(Exception):
"""Base class for restic errors
Args:
proc (CalledProcessError | CompletedProcess): subprocess result
restic: (Restic object)
"""
def __init__(
self,
proc: Union[CalledProcessError, CompletedProcess],
restic: 'Restic',
):
super().__init__(proc.stderr)
self.stderr = proc.stderr
self.returncode = proc.returncode
self.restic = restic
def __new__(
cls, proc: Union[CalledProcessError, CompletedProcess], *_, **__
):
if cls is not ResticError:
return Exception.__new__(cls)
stderr = proc.stderr.strip()
if (
'dial tcp' in stderr
or 'TLS handshake timeout' in stderr
or '502 Bad Gateway' in stderr
):
return Exception.__new__(ResticConnError)
if 'The access key ID you provided does not exist' in stderr:
return Exception.__new__(ResticAccessError)
if (
'Stat: The specified key does not exist' in stderr
or 'The specified bucket does not exist' in stderr
):
return Exception.__new__(ResticInitError)
if 'unable to create lock in backend' in stderr:
return Exception.__new__(ResticLockedError)
index_errs = (
'rebuild-index',
'unable to load index',
'returned error, retrying after',
'Load(<index',
'Load(<lock',
)
if any(x in stderr for x in index_errs) or (
stderr.startswith('Fatal:') and 'invalid data returned' in stderr
):
return Exception.__new__(ResticBadIndexError)
if proc.returncode < 0 and abs(proc.returncode) == signal.SIGPIPE:
# https://github.com/restic/restic/issues/1466
# https://github.com/restic/restic/pull/2546
return Exception.__new__(ResticConnError)
return Exception.__new__(cls)
def __str__(self):
name = type(self).__name__
if self.returncode < 0:
signum = abs(self.returncode)
try:
# https://github.com/PyCQA/pylint/issues/2804
sig = signal.Signals(signum).name # pylint: disable=no-member
except ValueError:
sig = 'signal %d' % signum
if signum == signal.SIGPIPE:
# https://github.com/restic/restic/issues/1466
return f'{name}: Timeout: received SIGPIPE. {self.stderr}'
return f'{name}: (killed with {sig}) {self.stderr}'
return f'{name}: (return code {self.returncode}) {self.stderr}'
class ResticBadIndexError(ResticError):
"""Raised when restic raises an index corruption error"""
class ResticAccessError(ResticError):
"""Raised when S3 access/secret keys are incorrect for restic"""
class ResticInitError(ResticError):
"""Raised when a restic repo connects but is not initialized"""
class ResticConnError(ResticError):
"""Raised when a connection is refused or times out from ceph"""
class ResticLockedError(ResticError):
"""Raised when an operation on a repo fails due to a lock held on it"""
def __init__(
self,
proc: Union[CalledProcessError, CompletedProcess],
restic: 'Restic',
):
super().__init__(proc, restic)
pid_re = re.compile(r'locked\ (?:exclusively\ )?by\ PID\ (\d+)\ ')
match = pid_re.search(proc.stderr)
if match:
self.pid = int(match.group(1))
else:
logging.warning('failed to parse PID from %r', proc.stderr)
self.pid = None
def unlock_ok(self) -> bool:
"""Check if a locked repo can be unlocked
Returns:
bool: true if restic unlock can be run safely
"""
if self.pid is None:
return False
try:
os.kill(self.pid, 0)
except ProcessLookupError:
return True
return False
Zerion Mini Shell 1.0