Mini Shell
"""Restic Exception Classes"""
from typing import Union
import os
import re
import platform
import logging
from signal import Signals, SIGPIPE
from subprocess import CalledProcessError, CompletedProcess
LOCK_RE = re.compile(r'locked (?:exclusively )?by PID (\d+) on ([\w\.\-]+) by')
class ResticError(Exception):
"""Base class for restic errors
Args:
proc (CalledProcessError | CompletedProcess): subprocess result
"""
__module__ = 'restic'
def __init__(self, proc: Union[CalledProcessError, CompletedProcess]):
super().__init__(proc.stderr)
self.stderr = proc.stderr
self.returncode = proc.returncode
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 = (
'repair index',
'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) == SIGPIPE:
# https://github.com/restic/restic/issues/1466
# https://github.com/restic/restic/pull/2546
return Exception.__new__(ResticConnError)
return Exception.__new__(cls)
@property
def name(self) -> str:
"""Get the name of the class of the restic exception
Returns:
str: class name
"""
return type(self).__name__
def __str__(self):
if self.returncode < 0:
signum = abs(self.returncode)
try:
# https://github.com/PyCQA/pylint/issues/2804
sig = Signals(signum).name # pylint: disable=no-member
except ValueError:
sig = f'signal {signum}'
if signum == SIGPIPE:
# https://github.com/restic/restic/issues/1466
return f'Timeout: received SIGPIPE. {self.stderr}'
return f'(killed with {sig}) {self.stderr}'
return f'(return code {self.returncode}) {self.stderr}'
class ResticBadIndexError(ResticError):
"""Raised when restic raises an index corruption error"""
__module__ = 'restic'
class ResticAccessError(ResticError):
"""Raised when S3 access/secret keys are incorrect for restic"""
__module__ = 'restic'
class ResticInitError(ResticError):
"""Raised when a restic repo connects but is not initialized"""
__module__ = 'restic'
class ResticConnError(ResticError):
"""Raised when a connection is refused or times out from ceph"""
__module__ = 'restic'
class ResticLockedError(ResticError):
"""Raised when an operation on a repo fails due to a lock held on it"""
__module__ = 'restic'
def __init__(self, proc: Union[CalledProcessError, CompletedProcess]):
super().__init__(proc)
if match := LOCK_RE.search(proc.stderr):
self.pid = int(match.group(1))
self.host = match.group(2)
else:
logging.warning('failed to parse PID and host from %r', proc.stderr)
self.pid = None
self.host = 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
if platform.node() != self.host:
return False
try:
os.kill(self.pid, 0)
except ProcessLookupError:
return True
return False
def __str__(self):
if self.pid:
return f"repo is in use and locked by PID {self.pid} on {self.host}"
return super().__str__()
Zerion Mini Shell 1.0